Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a0991c259 | ||
|
|
2e454c9723 | ||
|
|
7e6d94c8e0 | ||
|
|
e18b790ccd | ||
|
|
0274417ad1 | ||
|
|
a8a3a93ace | ||
|
|
88bbab216e | ||
|
|
f74434a282 | ||
|
|
ed6f9d8252 | ||
|
|
a19ccab16a | ||
|
|
77369a13f1 | ||
|
|
8231356d2a | ||
|
|
469a942b5a | ||
|
|
0afe6cf5d1 | ||
|
|
75a161fe35 | ||
|
|
eb1ed107ba | ||
|
|
13e2e340ac | ||
|
|
0af62da16b | ||
|
|
9b323cbcc8 | ||
|
|
18a0e2d75c | ||
|
|
87c89a9968 | ||
|
|
e237749abd | ||
|
|
994a36d039 | ||
|
|
c989817373 | ||
|
|
e3fb7340aa | ||
|
|
e4ef2832cd | ||
|
|
881602cf1c | ||
|
|
4c01f171ae | ||
|
|
d9817fcb07 | ||
|
|
a784af3e47 | ||
|
|
c268925330 | ||
|
|
378e8e0330 |
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
@@ -199,7 +199,7 @@ There's some settings that you'll need to setup.
|
|||||||
Under Vercel > Your API Project > Settings
|
Under Vercel > Your API Project > Settings
|
||||||
|
|
||||||
In General > Build & Development Settings
|
In General > Build & Development Settings
|
||||||
BUILD COMMAND: `yarn turbo run build --scope=@calcom/api --include-dependencies --no-deps`
|
BUILD COMMAND: `yarn turbo run build --filter=@calcom/api --no-deps`
|
||||||
OUTPUT DIRECTORY: `apps/api/.next`
|
OUTPUT DIRECTORY: `apps/api/.next`
|
||||||
|
|
||||||
In Git > Ignored Build Step
|
In Git > Ignored Build Step
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@sentry/nextjs": "^9.15.0",
|
"@sentry/nextjs": "^9.15.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "^15.3.0",
|
"next": "^15.4.5",
|
||||||
"next-api-middleware": "^1.0.1",
|
"next-api-middleware": "^1.0.1",
|
||||||
"next-axiom": "^0.17.0",
|
"next-axiom": "^0.17.0",
|
||||||
"next-swagger-doc": "^0.3.6",
|
"next-swagger-doc": "^0.3.6",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.300",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.314",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
|
|||||||
+8
-28
@@ -4,37 +4,21 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
|
|||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
||||||
import {
|
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
|
||||||
Body,
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
Controller,
|
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
Param,
|
|
||||||
ParseIntPipe,
|
|
||||||
Patch,
|
|
||||||
Post,
|
|
||||||
UseGuards,
|
|
||||||
} from "@nestjs/common";
|
|
||||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags, OmitType } from "@nestjs/swagger";
|
|
||||||
|
|
||||||
import {
|
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
EVENT_TYPE_READ,
|
|
||||||
EVENT_TYPE_WRITE,
|
|
||||||
SUCCESS_STATUS,
|
|
||||||
} from "@calcom/platform-constants";
|
|
||||||
import {
|
import {
|
||||||
CreatePrivateLinkInput,
|
CreatePrivateLinkInput,
|
||||||
CreatePrivateLinkOutput,
|
CreatePrivateLinkOutput,
|
||||||
DeletePrivateLinkOutput,
|
DeletePrivateLinkOutput,
|
||||||
GetPrivateLinksOutput,
|
GetPrivateLinksOutput,
|
||||||
UpdatePrivateLinkInput,
|
|
||||||
UpdatePrivateLinkOutput,
|
UpdatePrivateLinkOutput,
|
||||||
|
UpdatePrivateLinkBody,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
import { PrivateLinksService } from "../services/private-links.service";
|
import { PrivateLinksService } from "../services/private-links.service";
|
||||||
|
|
||||||
class UpdatePrivateLinkBody extends OmitType(UpdatePrivateLinkInput, ["linkId"] as const) {}
|
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/v2/event-types/:eventTypeId/private-links",
|
path: "/v2/event-types/:eventTypeId/private-links",
|
||||||
})
|
})
|
||||||
@@ -85,11 +69,10 @@ export class EventTypesPrivateLinksController {
|
|||||||
async updatePrivateLink(
|
async updatePrivateLink(
|
||||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
@Param("linkId") linkId: string,
|
@Param("linkId") linkId: string,
|
||||||
@Body() body: UpdatePrivateLinkBody,
|
@Body() body: UpdatePrivateLinkBody
|
||||||
@GetUser("id") userId: number
|
|
||||||
): Promise<UpdatePrivateLinkOutput> {
|
): Promise<UpdatePrivateLinkOutput> {
|
||||||
const updateInput = { ...body, linkId };
|
const updateInput = { ...body, linkId };
|
||||||
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, userId, updateInput);
|
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, updateInput);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
@@ -104,10 +87,9 @@ export class EventTypesPrivateLinksController {
|
|||||||
@ApiOperation({ summary: "Delete a private link for an event type" })
|
@ApiOperation({ summary: "Delete a private link for an event type" })
|
||||||
async deletePrivateLink(
|
async deletePrivateLink(
|
||||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
@Param("linkId") linkId: string,
|
@Param("linkId") linkId: string
|
||||||
@GetUser("id") userId: number
|
|
||||||
): Promise<DeletePrivateLinkOutput> {
|
): Promise<DeletePrivateLinkOutput> {
|
||||||
await this.privateLinksService.deletePrivateLink(eventTypeId, userId, linkId);
|
await this.privateLinksService.deletePrivateLink(eventTypeId, linkId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
@@ -118,5 +100,3 @@ export class EventTypesPrivateLinksController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import {
|
||||||
|
OPTIONAL_API_KEY_HEADER,
|
||||||
|
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
|
||||||
|
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
|
||||||
|
} from "@/lib/docs/headers";
|
||||||
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
|
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||||
|
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||||
|
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||||
|
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
|
||||||
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import {
|
||||||
|
CreatePrivateLinkInput,
|
||||||
|
CreatePrivateLinkOutput,
|
||||||
|
DeletePrivateLinkOutput,
|
||||||
|
GetPrivateLinksOutput,
|
||||||
|
UpdatePrivateLinkInput,
|
||||||
|
UpdatePrivateLinkOutput,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
|
import { PrivateLinksService } from "../services/private-links.service";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@DocsTags("Orgs / Teams / Event Types / Private Links")
|
||||||
|
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||||
|
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||||
|
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||||
|
export class OrganizationsEventTypesPrivateLinksController {
|
||||||
|
constructor(
|
||||||
|
private readonly privateLinksService: PrivateLinksService,
|
||||||
|
private readonly teamsEventTypesService: TeamsEventTypesService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post("/")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@ApiOperation({ summary: "Create a private link for a team event type" })
|
||||||
|
async createPrivateLink(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
|
@Body() body: CreatePrivateLinkInput
|
||||||
|
): Promise<CreatePrivateLinkOutput> {
|
||||||
|
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
|
||||||
|
// Use teamId as the seed for link generation in org/team context
|
||||||
|
const privateLink = await this.privateLinksService.createPrivateLink(eventTypeId, teamId, body);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: privateLink,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@ApiOperation({ summary: "Get all private links for a team event type" })
|
||||||
|
async getPrivateLinks(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
|
||||||
|
): Promise<GetPrivateLinksOutput> {
|
||||||
|
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
|
||||||
|
const privateLinks = await this.privateLinksService.getPrivateLinks(eventTypeId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: privateLinks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("/:linkId")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@ApiOperation({ summary: "Update a private link for a team event type" })
|
||||||
|
async updatePrivateLink(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
|
@Param("linkId") linkId: string,
|
||||||
|
@Body() body: Omit<UpdatePrivateLinkInput, "linkId">
|
||||||
|
): Promise<UpdatePrivateLinkOutput> {
|
||||||
|
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
|
||||||
|
const updateInput: UpdatePrivateLinkInput = { ...body, linkId };
|
||||||
|
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, updateInput);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: privateLink,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete("/:linkId")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@ApiOperation({ summary: "Delete a private link for a team event type" })
|
||||||
|
async deletePrivateLink(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
|
@Param("linkId") linkId: string
|
||||||
|
): Promise<DeletePrivateLinkOutput> {
|
||||||
|
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
|
||||||
|
await this.privateLinksService.deletePrivateLink(eventTypeId, linkId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: {
|
||||||
|
linkId,
|
||||||
|
message: "Private link deleted successfully",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import { PrivateLinksOutputService } from "./services/private-links-output.servi
|
|||||||
import { PrivateLinksService } from "./services/private-links.service";
|
import { PrivateLinksService } from "./services/private-links.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TokensModule, OAuthClientModule, PrismaModule, EventTypesModule_2024_06_14],
|
imports: [TokensModule, PrismaModule, EventTypesModule_2024_06_14],
|
||||||
controllers: [EventTypesPrivateLinksController],
|
controllers: [EventTypesPrivateLinksController],
|
||||||
providers: [
|
providers: [
|
||||||
PrivateLinksService,
|
PrivateLinksService,
|
||||||
@@ -21,5 +21,6 @@ import { PrivateLinksService } from "./services/private-links.service";
|
|||||||
PrivateLinksRepository,
|
PrivateLinksRepository,
|
||||||
EventTypeOwnershipGuard,
|
EventTypeOwnershipGuard,
|
||||||
],
|
],
|
||||||
|
exports: [PrivateLinksService],
|
||||||
})
|
})
|
||||||
export class EventTypesPrivateLinksModule {}
|
export class EventTypesPrivateLinksModule {}
|
||||||
|
|||||||
@@ -68,11 +68,7 @@ export class PrivateLinksService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePrivateLink(
|
async updatePrivateLink(eventTypeId: number, input: UpdatePrivateLinkInput): Promise<PrivateLinkOutput> {
|
||||||
eventTypeId: number,
|
|
||||||
userId: number,
|
|
||||||
input: UpdatePrivateLinkInput
|
|
||||||
): Promise<PrivateLinkOutput> {
|
|
||||||
try {
|
try {
|
||||||
const transformedInput = this.inputService.transformUpdateInput(input);
|
const transformedInput = this.inputService.transformUpdateInput(input);
|
||||||
const updatedResult = await this.repo.update(eventTypeId, {
|
const updatedResult = await this.repo.update(eventTypeId, {
|
||||||
@@ -106,7 +102,7 @@ export class PrivateLinksService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deletePrivateLink(eventTypeId: number, userId: number, linkId: string): Promise<void> {
|
async deletePrivateLink(eventTypeId: number, linkId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { count } = await this.repo.delete(eventTypeId, linkId);
|
const { count } = await this.repo.delete(eventTypeId, linkId);
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||||
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||||
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||||
|
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
|
||||||
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||||
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
||||||
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
||||||
@@ -12,6 +13,7 @@ import { BusyTimesService } from "@/lib/services/busy-times.service";
|
|||||||
import { CacheService } from "@/lib/services/cache.service";
|
import { CacheService } from "@/lib/services/cache.service";
|
||||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||||
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
||||||
|
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||||
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
@@ -31,6 +33,7 @@ import { Module } from "@nestjs/common";
|
|||||||
PrismaTeamRepository,
|
PrismaTeamRepository,
|
||||||
RedisService,
|
RedisService,
|
||||||
PrismaFeaturesRepository,
|
PrismaFeaturesRepository,
|
||||||
|
PrismaMembershipRepository,
|
||||||
CheckBookingLimitsService,
|
CheckBookingLimitsService,
|
||||||
CacheService,
|
CacheService,
|
||||||
AvailableSlotsService,
|
AvailableSlotsService,
|
||||||
@@ -38,6 +41,7 @@ import { Module } from "@nestjs/common";
|
|||||||
BusyTimesService,
|
BusyTimesService,
|
||||||
FilterHostsService,
|
FilterHostsService,
|
||||||
QualifiedHostsService,
|
QualifiedHostsService,
|
||||||
|
NoSlotsNotificationService,
|
||||||
],
|
],
|
||||||
exports: [AvailableSlotsService],
|
exports: [AvailableSlotsService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaMembershipRepository as PrismaMembershipRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaMembershipRepository extends PrismaMembershipRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"
|
|||||||
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
||||||
import { CacheService } from "@/lib/services/cache.service";
|
import { CacheService } from "@/lib/services/cache.service";
|
||||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||||
|
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||||
import { RedisService } from "@/modules/redis/redis.service";
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
@@ -35,7 +36,8 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
|||||||
checkBookingLimitsService: CheckBookingLimitsService,
|
checkBookingLimitsService: CheckBookingLimitsService,
|
||||||
cacheService: CacheService,
|
cacheService: CacheService,
|
||||||
userAvailabilityService: UserAvailabilityService,
|
userAvailabilityService: UserAvailabilityService,
|
||||||
busyTimesService: BusyTimesService
|
busyTimesService: BusyTimesService,
|
||||||
|
noSlotsNotificationService: NoSlotsNotificationService
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
oooRepo: oooRepoDependency,
|
oooRepo: oooRepoDependency,
|
||||||
@@ -53,6 +55,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
|||||||
busyTimesService,
|
busyTimesService,
|
||||||
qualifiedHostsService,
|
qualifiedHostsService,
|
||||||
featuresRepo: featuresRepository,
|
featuresRepo: featuresRepository,
|
||||||
|
noSlotsNotificationService,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
|
||||||
|
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||||
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { NoSlotsNotificationService as BaseNoSlotsNotificationService } from "@calcom/platform-libraries/slots";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NoSlotsNotificationService extends BaseNoSlotsNotificationService {
|
||||||
|
constructor(
|
||||||
|
teamRepository: PrismaTeamRepository,
|
||||||
|
membershipRepository: PrismaMembershipRepository,
|
||||||
|
redisService: RedisService
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
teamRepo: teamRepository,
|
||||||
|
membershipRepo: membershipRepository,
|
||||||
|
redisClient: redisService,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+551
@@ -0,0 +1,551 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||||
|
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||||
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
|
import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.module";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||||
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
|
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
|
||||||
|
import { ManagedOrganizationsRepositoryFixture } from "test/fixtures/repository/managed-organizations.repository.fixture";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
|
import { randomString } from "@calcom/lib/random";
|
||||||
|
import {
|
||||||
|
CAL_API_VERSION_HEADER,
|
||||||
|
SUCCESS_STATUS,
|
||||||
|
VERSION_2024_08_13,
|
||||||
|
X_CAL_CLIENT_ID,
|
||||||
|
X_CAL_SECRET_KEY,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
|
import {
|
||||||
|
BookingOutput_2024_08_13,
|
||||||
|
RecurringBookingOutput_2024_08_13,
|
||||||
|
GetBookingsOutput_2024_08_13,
|
||||||
|
GetSeatedBookingOutput_2024_08_13,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||||
|
describe("Manager and managed organizations bookings", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let managerOrganization: Team;
|
||||||
|
let managedOrganization: Team;
|
||||||
|
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
|
let schedulesService: SchedulesService_2024_04_15;
|
||||||
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
|
let oAuthClient: PlatformOAuthClient;
|
||||||
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
let hostsRepositoryFixture: HostsRepositoryFixture;
|
||||||
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
|
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let managedOrganizationsRepositoryFixture: ManagedOrganizationsRepositoryFixture;
|
||||||
|
|
||||||
|
const managerOrgUserEmail = "manager-org-user-1-bookings@api.com";
|
||||||
|
const managerOrgUserEmail2 = "manager-org-user-2-bookings@api.com";
|
||||||
|
const managedOrgUserEmail = "managed-org-user-1-bookings@api.com";
|
||||||
|
const nonOrgUserEmail1 = "non-org-user-1-bookings@api.com";
|
||||||
|
let managerOrgUser1: User;
|
||||||
|
let managerOrgUser2: User;
|
||||||
|
let managedOrgUser: User;
|
||||||
|
let nonOrgUser1: User;
|
||||||
|
let managerOrgTeam1: Team;
|
||||||
|
let managedOrgTeam1: Team;
|
||||||
|
|
||||||
|
let managerOrgEventTypeId: number;
|
||||||
|
let managedOrgEventTypeId: number;
|
||||||
|
let nonOrgEventTypeId: number;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
managerOrgUserEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, OrganizationsTeamsBookingsModule],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.overrideGuard(PermissionsGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
.overrideGuard(PlatformPlanGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
|
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||||
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
|
||||||
|
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||||
|
managedOrganizationsRepositoryFixture = new ManagedOrganizationsRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
await setupManagerOrganization();
|
||||||
|
await setupManagedOrganization();
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setupManagerOrganization() {
|
||||||
|
managerOrganization = await organizationsRepositoryFixture.create({
|
||||||
|
name: "organization bookings",
|
||||||
|
isPlatform: true,
|
||||||
|
});
|
||||||
|
managerOrgTeam1 = await teamRepositoryFixture.create({
|
||||||
|
name: "team orgs booking 1",
|
||||||
|
isOrganization: false,
|
||||||
|
parent: { connect: { id: managerOrganization.id } },
|
||||||
|
});
|
||||||
|
oAuthClient = await createOAuthClient(managerOrganization.id);
|
||||||
|
|
||||||
|
nonOrgUser1 = await userRepositoryFixture.create({
|
||||||
|
email: nonOrgUserEmail1,
|
||||||
|
locale: "it",
|
||||||
|
name: "NonOrgUser1Bookings",
|
||||||
|
platformOAuthClients: {
|
||||||
|
connect: {
|
||||||
|
id: oAuthClient.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
managerOrgUser1 = await userRepositoryFixture.create({
|
||||||
|
email: managerOrgUserEmail,
|
||||||
|
locale: "it",
|
||||||
|
name: "orgUser1Bookings",
|
||||||
|
platformOAuthClients: {
|
||||||
|
connect: {
|
||||||
|
id: oAuthClient.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
managerOrgUser2 = await userRepositoryFixture.create({
|
||||||
|
email: managerOrgUserEmail2,
|
||||||
|
locale: "it",
|
||||||
|
name: "orgUser2Bookings",
|
||||||
|
platformOAuthClients: {
|
||||||
|
connect: {
|
||||||
|
id: oAuthClient.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||||
|
name: "working time",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
isDefault: true,
|
||||||
|
};
|
||||||
|
await schedulesService.createUserSchedule(managerOrgUser1.id, userSchedule);
|
||||||
|
await schedulesService.createUserSchedule(nonOrgUser1.id, userSchedule);
|
||||||
|
await schedulesService.createUserSchedule(managerOrgUser2.id, userSchedule);
|
||||||
|
|
||||||
|
const orgEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||||
|
schedulingType: "COLLECTIVE",
|
||||||
|
team: {
|
||||||
|
connect: { id: managerOrgTeam1.id },
|
||||||
|
},
|
||||||
|
title: "Collective Event Type",
|
||||||
|
slug: "manager-org-bookings-collective-event-type",
|
||||||
|
length: 60,
|
||||||
|
assignAllTeamMembers: true,
|
||||||
|
bookingFields: [],
|
||||||
|
locations: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await profileRepositoryFixture.create({
|
||||||
|
uid: `usr-${managerOrgUser1.id}`,
|
||||||
|
username: managerOrgUserEmail,
|
||||||
|
organization: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrganization.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgUser1.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await profileRepositoryFixture.create({
|
||||||
|
uid: `usr-${managerOrgUser2.id}`,
|
||||||
|
username: managerOrgUserEmail2,
|
||||||
|
organization: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrganization.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgUser2.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "OWNER",
|
||||||
|
user: { connect: { id: managerOrgUser1.id } },
|
||||||
|
team: { connect: { id: managerOrganization.id } },
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "MEMBER",
|
||||||
|
user: { connect: { id: managerOrgUser2.id } },
|
||||||
|
team: { connect: { id: managerOrganization.id } },
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nonOrgEventType = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: "Non Org Event Type",
|
||||||
|
slug: "non-org-event-type",
|
||||||
|
length: 60,
|
||||||
|
bookingFields: [],
|
||||||
|
locations: [],
|
||||||
|
},
|
||||||
|
nonOrgUser1.id
|
||||||
|
);
|
||||||
|
|
||||||
|
managerOrgEventTypeId = orgEventType.id;
|
||||||
|
nonOrgEventTypeId = nonOrgEventType.id;
|
||||||
|
|
||||||
|
await hostsRepositoryFixture.create({
|
||||||
|
isFixed: true,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgUser2.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgEventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgUser2.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 9, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)),
|
||||||
|
title: "Manager Org Collective Booking",
|
||||||
|
uid: `manager-org-collective-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgEventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: "alice",
|
||||||
|
email: "alice@gmail.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "alice@gmail.com",
|
||||||
|
name: "alice",
|
||||||
|
locale: "es",
|
||||||
|
timeZone: "Europe/Madrid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: nonOrgUser1.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)),
|
||||||
|
title: "Non-Org Booking 1",
|
||||||
|
uid: `non-org-booking-1-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: nonOrgEventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/jkl-mno-pqr",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: managerOrgUser2.name ?? "",
|
||||||
|
email: managerOrgUserEmail2,
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: managerOrgUserEmail2,
|
||||||
|
name: managerOrgUser2.name ?? "",
|
||||||
|
locale: "en",
|
||||||
|
timeZone: managerOrgUser2.timeZone ?? "Europe/Madrid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupManagedOrganization() {
|
||||||
|
managedOrganization = await organizationsRepositoryFixture.create({
|
||||||
|
name: "organization bookings",
|
||||||
|
isPlatform: true,
|
||||||
|
});
|
||||||
|
await managedOrganizationsRepositoryFixture.createManagedOrganization(
|
||||||
|
managerOrganization.id,
|
||||||
|
managedOrganization.id
|
||||||
|
);
|
||||||
|
|
||||||
|
managedOrgTeam1 = await teamRepositoryFixture.create({
|
||||||
|
name: "managed org team 1",
|
||||||
|
isOrganization: false,
|
||||||
|
parent: { connect: { id: managedOrganization.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
managedOrgUser = await userRepositoryFixture.create({
|
||||||
|
email: managedOrgUserEmail,
|
||||||
|
locale: "it",
|
||||||
|
name: "ManagedOrgUser1Bookings",
|
||||||
|
platformOAuthClients: {
|
||||||
|
connect: {
|
||||||
|
id: oAuthClient.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await profileRepositoryFixture.create({
|
||||||
|
uid: `usr-${managedOrgUser.id}`,
|
||||||
|
username: managedOrgUserEmail,
|
||||||
|
organization: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrganization.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrgUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await profileRepositoryFixture.create({
|
||||||
|
uid: `usr-${managerOrgUser1.id}`,
|
||||||
|
username: managerOrgUserEmail,
|
||||||
|
organization: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrganization.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgUser1.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "MEMBER",
|
||||||
|
user: { connect: { id: managedOrgUser.id } },
|
||||||
|
team: { connect: { id: managedOrganization.id } },
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "OWNER",
|
||||||
|
user: { connect: { id: managerOrgUser1.id } },
|
||||||
|
team: { connect: { id: managedOrganization.id } },
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||||
|
name: "working time",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
isDefault: true,
|
||||||
|
};
|
||||||
|
await schedulesService.createUserSchedule(managedOrgUser.id, userSchedule);
|
||||||
|
|
||||||
|
const managedOrgEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||||
|
schedulingType: "COLLECTIVE",
|
||||||
|
team: {
|
||||||
|
connect: { id: managedOrgTeam1.id },
|
||||||
|
},
|
||||||
|
title: "Collective Event Type",
|
||||||
|
slug: "managed-org-bookings-collective-event-type",
|
||||||
|
length: 60,
|
||||||
|
assignAllTeamMembers: true,
|
||||||
|
bookingFields: [],
|
||||||
|
locations: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
managedOrgEventTypeId = managedOrgEventType.id;
|
||||||
|
|
||||||
|
await hostsRepositoryFixture.create({
|
||||||
|
isFixed: true,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrgUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrgEventType.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrgUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 11, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 11, 14, 0, 0)),
|
||||||
|
title: "Managed Org Booking",
|
||||||
|
uid: `managed-org-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrgEventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/ghi-jkl-mno",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: "charlie",
|
||||||
|
email: "charlie@gmail.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "charlie@gmail.com",
|
||||||
|
name: "charlie",
|
||||||
|
locale: "en",
|
||||||
|
timeZone: "Europe/Madrid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("get manager organization bookings", () => {
|
||||||
|
it("should get bookings by organizationId for manager organization", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${managerOrganization.id}/bookings`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
const data: (
|
||||||
|
| BookingOutput_2024_08_13
|
||||||
|
| RecurringBookingOutput_2024_08_13
|
||||||
|
| GetSeatedBookingOutput_2024_08_13
|
||||||
|
)[] = responseBody.data;
|
||||||
|
expect(data.length).toEqual(2);
|
||||||
|
expect(data.map((booking) => booking.eventTypeId).sort()).toEqual(
|
||||||
|
[managerOrgEventTypeId, nonOrgEventTypeId].sort()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get bookings by organizationId and userId", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${managerOrganization.id}/bookings?userIds=${managerOrgUser2.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
const data: (
|
||||||
|
| BookingOutput_2024_08_13
|
||||||
|
| RecurringBookingOutput_2024_08_13
|
||||||
|
| GetSeatedBookingOutput_2024_08_13
|
||||||
|
)[] = responseBody.data;
|
||||||
|
expect(data.length).toEqual(2);
|
||||||
|
expect(data.map((booking) => booking.eventTypeId).sort()).toEqual(
|
||||||
|
[managerOrgEventTypeId, nonOrgEventTypeId].sort()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("get managed organization bookings", () => {
|
||||||
|
it("should get bookings by organizationId for managed organization", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${managedOrganization.id}/bookings`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
const data: (
|
||||||
|
| BookingOutput_2024_08_13
|
||||||
|
| RecurringBookingOutput_2024_08_13
|
||||||
|
| GetSeatedBookingOutput_2024_08_13
|
||||||
|
)[] = responseBody.data;
|
||||||
|
expect(data.length).toEqual(1);
|
||||||
|
expect(data[0].eventTypeId).toEqual(managedOrgEventTypeId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createOAuthClient(organizationId: number) {
|
||||||
|
const data = {
|
||||||
|
logo: "logo-url",
|
||||||
|
name: "name",
|
||||||
|
redirectUris: ["http://localhost:5555"],
|
||||||
|
permissions: 32,
|
||||||
|
areCalendarEventsEnabled: false,
|
||||||
|
areEmailsEnabled: false,
|
||||||
|
};
|
||||||
|
const secret = "secret";
|
||||||
|
|
||||||
|
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
|
await teamRepositoryFixture.delete(managerOrganization.id);
|
||||||
|
await userRepositoryFixture.deleteByEmail(managerOrgUser1.email);
|
||||||
|
await userRepositoryFixture.deleteByEmail(nonOrgUser1.email);
|
||||||
|
await bookingsRepositoryFixture.deleteAllBookings(managerOrgUser1.id, managerOrgUser1.email);
|
||||||
|
await bookingsRepositoryFixture.deleteAllBookings(nonOrgUser1.id, nonOrgUser1.email);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
|
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { randomString } from "test/utils/randomString";
|
||||||
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { CreatePrivateLinkInput } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
describe("Organizations / Teams / Event Types / Private Links Endpoints", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let orgFixture: OrganizationRepositoryFixture;
|
||||||
|
let teamFixture: TeamRepositoryFixture;
|
||||||
|
let userFixture: UserRepositoryFixture;
|
||||||
|
let eventTypesFixture: EventTypesRepositoryFixture;
|
||||||
|
|
||||||
|
let org: any;
|
||||||
|
let team: any;
|
||||||
|
let user: any;
|
||||||
|
let eventType: any;
|
||||||
|
|
||||||
|
const userEmail = `org-private-links-user-${randomString()}@api.com`;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const testingModuleBuilder = withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
|
imports: [AppModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// Bypass org admin plan and admin API checks and roles in this e2e
|
||||||
|
.overrideGuard(PlatformPlanGuard)
|
||||||
|
.useValue({ canActivate: () => true })
|
||||||
|
.overrideGuard(IsAdminAPIEnabledGuard)
|
||||||
|
.useValue({ canActivate: () => true })
|
||||||
|
.overrideGuard(RolesGuard)
|
||||||
|
.useValue({ canActivate: () => true })
|
||||||
|
// Keep IsOrgGuard and IsTeamInOrg to validate org/team path integrity
|
||||||
|
.overrideGuard(ApiAuthGuard)
|
||||||
|
.useValue({ canActivate: () => true });
|
||||||
|
|
||||||
|
const moduleRef = await testingModuleBuilder.compile();
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
orgFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
teamFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
userFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
eventTypesFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
user = await userFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
username: `org-private-links-user-${randomString()}`,
|
||||||
|
name: "Test User",
|
||||||
|
});
|
||||||
|
|
||||||
|
org = await orgFixture.create({
|
||||||
|
name: `org-private-links-org-${randomString()}`,
|
||||||
|
slug: `org-private-links-org-${randomString()}`,
|
||||||
|
isOrganization: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
team = await teamFixture.create({
|
||||||
|
name: `org-private-links-team-${randomString()}`,
|
||||||
|
isOrganization: false,
|
||||||
|
parent: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a team-owned event type
|
||||||
|
eventType = await eventTypesFixture.createTeamEventType({
|
||||||
|
title: `org-private-links-event-type-${randomString()}`,
|
||||||
|
slug: `org-private-links-event-type-${randomString()}`,
|
||||||
|
length: 30,
|
||||||
|
locations: [],
|
||||||
|
team: { connect: { id: team.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("POST /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links - create", async () => {
|
||||||
|
const body: CreatePrivateLinkInput = { maxUsageCount: 5 };
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`)
|
||||||
|
.set("Authorization", "Bearer test")
|
||||||
|
.send(body)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||||
|
expect(response.body.data.linkId).toBeDefined();
|
||||||
|
expect(response.body.data.maxUsageCount).toBe(5);
|
||||||
|
expect(response.body.data.usageCount).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("GET /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links - list", async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`)
|
||||||
|
.set("Authorization", "Bearer test")
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||||
|
expect(Array.isArray(response.body.data)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PATCH /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links/:linkId - update", async () => {
|
||||||
|
// create first
|
||||||
|
const createResp = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`)
|
||||||
|
.set("Authorization", "Bearer test")
|
||||||
|
.send({ maxUsageCount: 3 })
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const linkId = createResp.body.data.linkId as string;
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.patch(
|
||||||
|
`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links/${linkId}`
|
||||||
|
)
|
||||||
|
.set("Authorization", "Bearer test")
|
||||||
|
.send({ maxUsageCount: 10 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||||
|
expect(response.body.data.maxUsageCount).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("DELETE /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links/:linkId - delete", async () => {
|
||||||
|
// create first
|
||||||
|
const createResp = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`)
|
||||||
|
.set("Authorization", "Bearer test")
|
||||||
|
.send({ maxUsageCount: 2 })
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const linkId = createResp.body.data.linkId as string;
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.delete(
|
||||||
|
`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links/${linkId}`
|
||||||
|
)
|
||||||
|
.set("Authorization", "Bearer test")
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||||
|
expect(response.body.data.linkId).toBe(linkId);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
try {
|
||||||
|
if (eventType?.id) {
|
||||||
|
await eventTypesFixture.delete(eventType.id);
|
||||||
|
}
|
||||||
|
if (team?.id) {
|
||||||
|
await teamFixture.delete(team.id);
|
||||||
|
}
|
||||||
|
if (org?.id) {
|
||||||
|
await orgFixture.delete(org.id);
|
||||||
|
}
|
||||||
|
if (user?.email) {
|
||||||
|
await userFixture.deleteByEmail(user.email);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
+17
-1
@@ -2,6 +2,8 @@ import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inpu
|
|||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||||
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
||||||
|
|
||||||
@@ -62,10 +64,24 @@ export class OrganizationsMembershipService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||||
const membership = await this.organizationsMembershipRepository.deleteOrgMembership(
|
// Get the membership first to get the userId
|
||||||
|
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
||||||
organizationId,
|
organizationId,
|
||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Membership with id ${membershipId} within organization id ${organizationId} not found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await TeamService.removeMembers({
|
||||||
|
teamIds: [organizationId],
|
||||||
|
userIds: [membership.userId],
|
||||||
|
isOrg: true,
|
||||||
|
});
|
||||||
|
|
||||||
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { OrganizationsConferencingModule } from "@/modules/organizations/confere
|
|||||||
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
||||||
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
|
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
|
||||||
import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller";
|
import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller";
|
||||||
|
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
|
||||||
|
import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller";
|
||||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
|
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
|
||||||
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
|
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
|
||||||
import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service";
|
import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service";
|
||||||
@@ -91,6 +93,7 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsStripeModule,
|
OrganizationsStripeModule,
|
||||||
OrganizationsTeamsRoutingFormsModule,
|
OrganizationsTeamsRoutingFormsModule,
|
||||||
OrganizationsConferencingModule,
|
OrganizationsConferencingModule,
|
||||||
|
EventTypesPrivateLinksModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
OrganizationsRepository,
|
OrganizationsRepository,
|
||||||
@@ -177,6 +180,7 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsTeamsSchedulesController,
|
OrganizationsTeamsSchedulesController,
|
||||||
OrganizationsUsersOOOController,
|
OrganizationsUsersOOOController,
|
||||||
OrganizationTeamWorkflowsController,
|
OrganizationTeamWorkflowsController,
|
||||||
|
OrganizationsEventTypesPrivateLinksController,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class OrganizationsModule {}
|
export class OrganizationsModule {}
|
||||||
|
|||||||
-4
@@ -20,7 +20,6 @@ import {
|
|||||||
OrgTeamMembershipOutputResponseDto,
|
OrgTeamMembershipOutputResponseDto,
|
||||||
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
||||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
||||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
|
||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
@@ -59,7 +58,6 @@ export class OrganizationsTeamsMembershipsController {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
||||||
private teamsEventTypesService: TeamsEventTypesService,
|
|
||||||
private readonly organizationsRepository: OrganizationsRepository
|
private readonly organizationsRepository: OrganizationsRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -127,8 +125,6 @@ export class OrganizationsTeamsMembershipsController {
|
|||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||||
|
|||||||
+13
-1
@@ -3,6 +3,8 @@ import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/member
|
|||||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationsTeamsMembershipsService {
|
export class OrganizationsTeamsMembershipsService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -58,11 +60,21 @@ export class OrganizationsTeamsMembershipsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
|
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
|
||||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.deleteOrgTeamMembershipById(
|
// First get the membership to get the userId
|
||||||
|
const teamMembership = await this.organizationsTeamsMembershipsRepository.findOrgTeamMembership(
|
||||||
organizationId,
|
organizationId,
|
||||||
teamId,
|
teamId,
|
||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!teamMembership) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Membership with id ${membershipId} not found in team ${teamId} of organization ${organizationId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||||
|
|
||||||
return teamMembership;
|
return teamMembership;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-1
@@ -12,7 +12,12 @@ import {
|
|||||||
EMAIL,
|
EMAIL,
|
||||||
WorkflowEmailAttendeeStepDto,
|
WorkflowEmailAttendeeStepDto,
|
||||||
} from "@/modules/workflows/inputs/workflow-step.input";
|
} from "@/modules/workflows/inputs/workflow-step.input";
|
||||||
import { BEFORE_EVENT, DAY } from "@/modules/workflows/inputs/workflow-trigger.input";
|
import {
|
||||||
|
BEFORE_EVENT,
|
||||||
|
DAY,
|
||||||
|
OnAfterEventTriggerDto,
|
||||||
|
OnBeforeEventTriggerDto,
|
||||||
|
} from "@/modules/workflows/inputs/workflow-trigger.input";
|
||||||
// Adjust path if needed
|
// Adjust path if needed
|
||||||
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
|
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
|
||||||
import { INestApplication } from "@nestjs/common";
|
import { INestApplication } from "@nestjs/common";
|
||||||
@@ -255,6 +260,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
|||||||
"+37255555555"
|
"+37255555555"
|
||||||
);
|
);
|
||||||
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
|
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
|
||||||
|
const trigger = sampleCreateWorkflowDto.trigger as OnBeforeEventTriggerDto;
|
||||||
|
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||||
|
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||||
|
|
||||||
createdWorkflowId = responseBody.data.id;
|
createdWorkflowId = responseBody.data.id;
|
||||||
createdWorkflow = responseBody.data;
|
createdWorkflow = responseBody.data;
|
||||||
});
|
});
|
||||||
@@ -330,6 +339,13 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
|||||||
expect(step1).toBeDefined();
|
expect(step1).toBeDefined();
|
||||||
const partialUpdateDto: Partial<CreateWorkflowDto> = {
|
const partialUpdateDto: Partial<CreateWorkflowDto> = {
|
||||||
name: updatedName,
|
name: updatedName,
|
||||||
|
trigger: {
|
||||||
|
type: "afterEvent",
|
||||||
|
offset: {
|
||||||
|
unit: "minute",
|
||||||
|
value: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
|
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
|
||||||
};
|
};
|
||||||
expect(createdWorkflowId).toBeDefined();
|
expect(createdWorkflowId).toBeDefined();
|
||||||
@@ -348,6 +364,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
|||||||
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
|
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
|
||||||
expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
|
expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
|
||||||
expect(responseBody.data.steps[1]?.id).toBeUndefined();
|
expect(responseBody.data.steps[1]?.id).toBeUndefined();
|
||||||
|
const trigger = partialUpdateDto.trigger as OnAfterEventTriggerDto;
|
||||||
|
expect(responseBody.data.trigger?.type).toEqual(trigger.type);
|
||||||
|
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||||
|
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -156,17 +156,4 @@ export class TeamsEventTypesService {
|
|||||||
|
|
||||||
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
|
|
||||||
try {
|
|
||||||
await this.teamsEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
|
|
||||||
await this.teamsEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error("Could not remove user from all team event-types.", {
|
|
||||||
error: err,
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-8
@@ -3,7 +3,6 @@ import { API_KEY_HEADER } from "@/lib/docs/headers";
|
|||||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
|
||||||
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
||||||
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
||||||
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
||||||
@@ -45,10 +44,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
|||||||
export class TeamsMembershipsController {
|
export class TeamsMembershipsController {
|
||||||
private logger = new Logger("TeamsMembershipsController");
|
private logger = new Logger("TeamsMembershipsController");
|
||||||
|
|
||||||
constructor(
|
constructor(private teamsMembershipsService: TeamsMembershipsService) {}
|
||||||
private teamsMembershipsService: TeamsMembershipsService,
|
|
||||||
private teamsEventTypesService: TeamsEventTypesService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -127,7 +123,6 @@ export class TeamsMembershipsController {
|
|||||||
membershipId,
|
membershipId,
|
||||||
body
|
body
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!currentMembership.accepted && updatedMembership.accepted) {
|
if (!currentMembership.accepted && updatedMembership.accepted) {
|
||||||
try {
|
try {
|
||||||
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
||||||
@@ -151,8 +146,6 @@ export class TeamsMembershipsController {
|
|||||||
): Promise<DeleteTeamMembershipOutput> {
|
): Promise<DeleteTeamMembershipOutput> {
|
||||||
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
|
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
|
||||||
|
|
||||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/up
|
|||||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamsMembershipsService {
|
export class TeamsMembershipsService {
|
||||||
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
||||||
@@ -41,10 +43,15 @@ export class TeamsMembershipsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteTeamMembership(teamId: number, membershipId: number) {
|
async deleteTeamMembership(teamId: number, membershipId: number) {
|
||||||
const teamMembership = await this.teamsMembershipsRepository.deleteTeamMembershipById(
|
// First get the membership to get the userId
|
||||||
teamId,
|
const teamMembership = await this.teamsMembershipsRepository.findTeamMembership(teamId, membershipId);
|
||||||
membershipId
|
|
||||||
);
|
if (!teamMembership) {
|
||||||
|
throw new NotFoundException(`Membership with id ${membershipId} not found in team ${teamId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||||
|
|
||||||
return teamMembership;
|
return teamMembership;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
|
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsNumber, ValidateNested } from "class-validator";
|
import { IsIn, IsNumber, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
export const BEFORE_EVENT = "beforeEvent";
|
export const BEFORE_EVENT = "beforeEvent";
|
||||||
export const EVENT_CANCELLED = "eventCancelled";
|
export const EVENT_CANCELLED = "eventCancelled";
|
||||||
@@ -68,6 +68,8 @@ export class WorkflowTriggerOffsetDto {
|
|||||||
value!: number;
|
value!: number;
|
||||||
|
|
||||||
@ApiProperty({ description: "Unit for the offset time", example: HOUR })
|
@ApiProperty({ description: "Unit for the offset time", example: HOUR })
|
||||||
|
@IsString()
|
||||||
|
@IsIn(TIME_UNITS)
|
||||||
unit!: TimeUnitType;
|
unit!: TimeUnitType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,8 @@ export class BaseWorkflowTriggerDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([WORKFLOW_TRIGGER_TYPES])
|
||||||
type!: WorkflowTriggerType;
|
type!: WorkflowTriggerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +86,8 @@ export class OnCreationTriggerDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([NEW_EVENT])
|
||||||
type: typeof NEW_EVENT = NEW_EVENT;
|
type: typeof NEW_EVENT = NEW_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,12 +95,16 @@ export class OnRescheduleTriggerDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([RESCHEDULE_EVENT])
|
||||||
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
|
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
|
||||||
}
|
}
|
||||||
export class OnCancelTriggerDto {
|
export class OnCancelTriggerDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([EVENT_CANCELLED])
|
||||||
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
|
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +123,8 @@ export class OnBeforeEventTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: BEFORE_EVENT,
|
example: BEFORE_EVENT,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([BEFORE_EVENT])
|
||||||
type: typeof BEFORE_EVENT = BEFORE_EVENT;
|
type: typeof BEFORE_EVENT = BEFORE_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +133,8 @@ export class OnAfterEventTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: AFTER_EVENT,
|
example: AFTER_EVENT,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([AFTER_EVENT])
|
||||||
type: typeof AFTER_EVENT = AFTER_EVENT;
|
type: typeof AFTER_EVENT = AFTER_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +143,8 @@ export class OnAfterCalVideoGuestsNoShowTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([AFTER_GUESTS_CAL_VIDEO_NO_SHOW])
|
||||||
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
|
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,5 +153,7 @@ export class OnAfterCalVideoHostsNoShowTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([AFTER_HOSTS_CAL_VIDEO_NO_SHOW])
|
||||||
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
|
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4384,6 +4384,292 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_createPrivateLink",
|
||||||
|
"summary": "Create a private link for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreatePrivateLinkInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreatePrivateLinkOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Orgs / Teams / Event Types / Private Links"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks",
|
||||||
|
"summary": "Get all private links for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/GetPrivateLinksOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Orgs / Teams / Event Types / Private Links"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": {
|
||||||
|
"patch": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_updatePrivateLink",
|
||||||
|
"summary": "Update a private link for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linkId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdatePrivateLinkOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Orgs / Teams / Event Types / Private Links"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink",
|
||||||
|
"summary": "Delete a private link for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linkId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/DeletePrivateLinkOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Orgs / Teams / Event Types / Private Links"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
|
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
|
||||||
@@ -23789,6 +24075,213 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CreatePrivateLinkInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Expiration date for time-based links",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2024-12-31T23:59:59.000Z"
|
||||||
|
},
|
||||||
|
"maxUsageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
||||||
|
"example": 10,
|
||||||
|
"minimum": 1,
|
||||||
|
"default": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TimeBasedPrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"linkId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The private link ID",
|
||||||
|
"example": "abc123def456"
|
||||||
|
},
|
||||||
|
"eventTypeId": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Event type ID this link belongs to",
|
||||||
|
"example": 123
|
||||||
|
},
|
||||||
|
"isExpired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the link is currently expired",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"bookingUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Full booking URL for this private link",
|
||||||
|
"format": "uri",
|
||||||
|
"example": "https://cal.com/d/abc123def456/30min"
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Expiration date for this time-based link",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2025-12-31T23:59:59.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"linkId",
|
||||||
|
"eventTypeId",
|
||||||
|
"isExpired",
|
||||||
|
"bookingUrl",
|
||||||
|
"expiresAt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"UsageBasedPrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"linkId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The private link ID",
|
||||||
|
"example": "abc123def456"
|
||||||
|
},
|
||||||
|
"eventTypeId": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Event type ID this link belongs to",
|
||||||
|
"example": 123
|
||||||
|
},
|
||||||
|
"isExpired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the link is currently expired",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"bookingUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Full booking URL for this private link",
|
||||||
|
"format": "uri",
|
||||||
|
"example": "https://cal.com/d/abc123def456/30min"
|
||||||
|
},
|
||||||
|
"maxUsageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum number of times this link can be used",
|
||||||
|
"example": 10
|
||||||
|
},
|
||||||
|
"usageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Current usage count for this link",
|
||||||
|
"example": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"linkId",
|
||||||
|
"eventTypeId",
|
||||||
|
"isExpired",
|
||||||
|
"bookingUrl",
|
||||||
|
"maxUsageCount",
|
||||||
|
"usageCount"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"CreatePrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"description": "Created private link data (either time-based or usage-based)",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"GetPrivateLinksOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of private links for the event type (mix of time-based and usage-based)",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"UpdatePrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"description": "Updated private link data (either time-based or usage-based)",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"DeletePrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Deleted link information",
|
||||||
|
"properties": {
|
||||||
|
"linkId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "abc123def456"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Private link deleted successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
"StripConnectOutputDto": {
|
"StripConnectOutputDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -25637,6 +26130,23 @@
|
|||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"UpdatePrivateLinkBody": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
"description": "New expiration date for time-based links",
|
||||||
|
"example": "2024-12-31T23:59:59.000Z"
|
||||||
|
},
|
||||||
|
"maxUsageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "New maximum number of times the link can be used",
|
||||||
|
"example": 10,
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"MeOrgOutput": {
|
"MeOrgOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -28282,230 +28792,6 @@
|
|||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"CreatePrivateLinkInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"expiresAt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Expiration date for time-based links",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2024-12-31T23:59:59.000Z"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
|
||||||
"example": 10,
|
|
||||||
"minimum": 1,
|
|
||||||
"default": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TimeBasedPrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The private link ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"eventTypeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Event type ID this link belongs to",
|
|
||||||
"example": 123
|
|
||||||
},
|
|
||||||
"isExpired": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the link is currently expired",
|
|
||||||
"example": false
|
|
||||||
},
|
|
||||||
"bookingUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full booking URL for this private link",
|
|
||||||
"format": "uri",
|
|
||||||
"example": "https://cal.com/d/abc123def456/30min"
|
|
||||||
},
|
|
||||||
"expiresAt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Expiration date for this time-based link",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2025-12-31T23:59:59.000Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"linkId",
|
|
||||||
"eventTypeId",
|
|
||||||
"isExpired",
|
|
||||||
"bookingUrl",
|
|
||||||
"expiresAt"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"UsageBasedPrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The private link ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"eventTypeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Event type ID this link belongs to",
|
|
||||||
"example": 123
|
|
||||||
},
|
|
||||||
"isExpired": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the link is currently expired",
|
|
||||||
"example": false
|
|
||||||
},
|
|
||||||
"bookingUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full booking URL for this private link",
|
|
||||||
"format": "uri",
|
|
||||||
"example": "https://cal.com/d/abc123def456/30min"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of times this link can be used",
|
|
||||||
"example": 10
|
|
||||||
},
|
|
||||||
"usageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Current usage count for this link",
|
|
||||||
"example": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"linkId",
|
|
||||||
"eventTypeId",
|
|
||||||
"isExpired",
|
|
||||||
"bookingUrl",
|
|
||||||
"maxUsageCount",
|
|
||||||
"usageCount"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"CreatePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Created private link data (either time-based or usage-based)",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"GetPrivateLinksOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Array of private links for the event type (mix of time-based and usage-based)",
|
|
||||||
"items": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"UpdatePrivateLinkBody": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"expiresAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string",
|
|
||||||
"description": "New expiration date for time-based links",
|
|
||||||
"example": "2024-12-31T23:59:59.000Z"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "New maximum number of times the link can be used",
|
|
||||||
"example": 10,
|
|
||||||
"minimum": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"UpdatePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Updated private link data (either time-based or usage-based)",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"DeletePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Deleted link information",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Private link deleted successfully"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"UserWebhookOutputDto": {
|
"UserWebhookOutputDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -59,4 +59,8 @@ export class EventTypesRepositoryFixture {
|
|||||||
async delete(eventTypeId: EventType["id"]) {
|
async delete(eventTypeId: EventType["id"]) {
|
||||||
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
|
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findById(eventTypeId: EventType["id"]) {
|
||||||
|
return this.prismaReadClient.eventType.findUnique({ where: { id: eventTypeId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -34,4 +34,13 @@ export class ManagedOrganizationsRepositoryFixture {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createManagedOrganization(managerOrganizationId: number, managedOrganizationId: number) {
|
||||||
|
return this.prismaWriteClient.managedOrganization.create({
|
||||||
|
data: {
|
||||||
|
managerOrganizationId,
|
||||||
|
managedOrganizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,4 +35,8 @@ export class MembershipRepositoryFixture {
|
|||||||
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
||||||
return membership;
|
return membership;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findById(membershipId: Membership["id"]) {
|
||||||
|
return this.prismaReadClient.membership.findUnique({ where: { id: membershipId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { createRouterCaller } from "app/_trpc/context";
|
import { createRouterCaller } from "app/_trpc/context";
|
||||||
import type { PageProps } from "app/_types";
|
import type { PageProps } from "app/_types";
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { AppCategories } from "@calcom/prisma/enums";
|
import { AppCategories } from "@calcom/prisma/enums";
|
||||||
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
||||||
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
||||||
|
|
||||||
|
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
@@ -31,16 +35,23 @@ const InstalledAppsWrapper = async ({ params }: PageProps) => {
|
|||||||
redirect("/apps/installed/calendar");
|
redirect("/apps/installed/calendar");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return redirect("/auth/login");
|
||||||
|
}
|
||||||
|
|
||||||
const [calendarsCaller, appsCaller] = await Promise.all([
|
const [calendarsCaller, appsCaller] = await Promise.all([
|
||||||
createRouterCaller(calendarsRouter),
|
createRouterCaller(calendarsRouter),
|
||||||
createRouterCaller(appsRouter),
|
createRouterCaller(appsRouter),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const connectedCalendars = await calendarsCaller.connectedCalendars();
|
const [connectedCalendars, installedCalendars] = await Promise.all([
|
||||||
const installedCalendars = await appsCaller.integrations({
|
calendarsCaller.connectedCalendars(),
|
||||||
variant: "calendar",
|
appsCaller.integrations({
|
||||||
onlyInstalled: true,
|
variant: "calendar",
|
||||||
});
|
onlyInstalled: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InstalledApps
|
<InstalledApps
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/about"
|
"/settings/organizations/new/about"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/add-teams"
|
"/settings/organizations/new/add-teams"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies } from "next/headers";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
|
||||||
|
|
||||||
@@ -18,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/handover"
|
"/settings/organizations/new/handover"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
+6
-12
@@ -1,21 +1,15 @@
|
|||||||
import type { GetServerSidePropsResult } from "next";
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
export const getServerSideProps = async (): Promise<GetServerSidePropsResult<{ isOrg: boolean }>> => {
|
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const featuresRepository = new FeaturesRepository(prisma);
|
const featuresRepository = new FeaturesRepository(prisma);
|
||||||
const organizations = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
const organizations = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
||||||
// Check if organizations are enabled
|
|
||||||
if (!organizations) {
|
if (!organizations) {
|
||||||
return {
|
return notFound();
|
||||||
notFound: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return children;
|
||||||
props: {
|
}
|
||||||
isOrg: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
+1
-10
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboard-members-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboard-members-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/onboard-members"
|
"/settings/organizations/new/onboard-members"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/create-new-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/create-new-view";
|
||||||
|
|
||||||
export const generateMetadata = async () =>
|
export const generateMetadata = async () =>
|
||||||
@@ -19,14 +13,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new"
|
"/settings/organizations/new"
|
||||||
);
|
);
|
||||||
|
|
||||||
type Props = {
|
const ServerPage = async () => {
|
||||||
isOrg: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getData = withAppDirSsr<Props>(getServerSideProps);
|
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/payment-status-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/payment-status-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/status"
|
"/settings/organizations/new/status"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ export type PageProps = {
|
|||||||
|
|
||||||
export type LayoutProps = { params: Promise<Params>; children: React.ReactElement };
|
export type LayoutProps = { params: Promise<Params>; children: React.ReactElement };
|
||||||
|
|
||||||
export { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
export type ReadonlyHeaders = Awaited<ReturnType<typeof import("next/headers").headers>>;
|
||||||
export { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
export type ReadonlyRequestCookies = Awaited<ReturnType<typeof import("next/headers").cookies>>;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
|||||||
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
|
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
|
||||||
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
||||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
||||||
|
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
|
||||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import { truncateOnWord } from "@calcom/lib/text";
|
import { truncateOnWord } from "@calcom/lib/text";
|
||||||
@@ -32,7 +33,7 @@ const _generateMetadataWithoutImage = async (
|
|||||||
const description = getDescription(t);
|
const description = getDescription(t);
|
||||||
const titleSuffix = `| ${APP_NAME}`;
|
const titleSuffix = `| ${APP_NAME}`;
|
||||||
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
|
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
|
||||||
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
|
const metadataBase = new URL(IS_CALCOM ? getCalcomUrl() : WEBAPP_URL);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: title.length === 0 ? APP_NAME : displayedTitle,
|
title: title.length === 0 ? APP_NAME : displayedTitle,
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ async function getHandler(request: NextRequest) {
|
|||||||
|
|
||||||
buffer = await optimizeImage({
|
buffer = await optimizeImage({
|
||||||
buffer,
|
buffer,
|
||||||
contentType: detectContentType(buffer) ?? "image/jpeg",
|
contentType: (await detectContentType(buffer)) ?? "image/jpeg",
|
||||||
quality: 100,
|
quality: 100,
|
||||||
width: logoDefinition.w,
|
width: logoDefinition.w,
|
||||||
height: logoDefinition.h, // optional
|
height: logoDefinition.h, // optional
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Script from "next/script";
|
|||||||
import "@calcom/embed-core/src/embed-iframe";
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
|
||||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||||
import { IconSprites } from "@calcom/ui/components/icon";
|
import { IconSprites } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ function PageWrapper(props: AppProps) {
|
|||||||
// Set canonical to https://cal.com or self-hosted URL
|
// Set canonical to https://cal.com or self-hosted URL
|
||||||
canonical={
|
canonical={
|
||||||
IS_CALCOM
|
IS_CALCOM
|
||||||
? buildCanonical({ path, origin: "https://cal.com" }) // cal.com & .dev
|
? buildCanonical({ path, origin: getCalcomUrl() }) // cal.com & .dev
|
||||||
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
|
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
|
||||||
}
|
}
|
||||||
{...seoConfig.defaultNextSeo}
|
{...seoConfig.defaultNextSeo}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { SearchParams } from "app/_types";
|
import type { SearchParams } from "app/_types";
|
||||||
import { type Params } from "app/_types";
|
import { type Params } from "app/_types";
|
||||||
|
import type { ReadonlyHeaders, ReadonlyRequestCookies } from "app/_types";
|
||||||
import type { GetServerSidePropsContext, NextApiRequest } from "next";
|
import type { GetServerSidePropsContext, NextApiRequest } from "next";
|
||||||
import { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
|
||||||
import { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
|
||||||
|
|
||||||
const createProxifiedObject = (object: Record<string, string>) =>
|
const createProxifiedObject = (object: Record<string, string>) =>
|
||||||
new Proxy(object, {
|
new Proxy(object, {
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
error: null,
|
|
||||||
migrated: null,
|
|
||||||
userId: session.user.id,
|
|
||||||
username: session.user.username,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -156,7 +156,7 @@ export function AvailabilityList({ availabilities }: AvailabilityListProps) {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-default mb-16 mt-4 hidden text-center text-sm md:block">
|
<div className="text-default mb-16 mt-4 block text-center text-sm">
|
||||||
{t("temporarily_out_of_office")}{" "}
|
{t("temporarily_out_of_office")}{" "}
|
||||||
<Link href="settings/my-account/out-of-office" className="underline">
|
<Link href="settings/my-account/out-of-office" className="underline">
|
||||||
{t("add_a_redirect")}
|
{t("add_a_redirect")}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "5.5.17",
|
"version": "5.6.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"next": "^15.3.0",
|
"next": "^15.4.5",
|
||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"next-axiom": "^0.17.0",
|
"next-axiom": "^0.17.0",
|
||||||
"next-collect": "^0.2.1",
|
"next-collect": "^0.2.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { IncomingMessage } from "http";
|
import type { IncomingMessage } from "http";
|
||||||
|
import type { NextPageContext } from "next";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
import type { AppContextType } from "next/dist/shared/lib/utils";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import CacheProvider from "react-inlinesvg/provider";
|
import CacheProvider from "react-inlinesvg/provider";
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MyApp.getInitialProps = async (ctx: AppContextType) => {
|
MyApp.getInitialProps = async ({ ctx }: { ctx: NextPageContext }) => {
|
||||||
const { req } = ctx.ctx;
|
const { req } = ctx;
|
||||||
|
|
||||||
let newLocale = "en";
|
let newLocale = "en";
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { test } from "./lib/fixtures";
|
|||||||
import {
|
import {
|
||||||
bookTimeSlot,
|
bookTimeSlot,
|
||||||
confirmReschedule,
|
confirmReschedule,
|
||||||
|
confirmBooking,
|
||||||
doOnOrgDomain,
|
doOnOrgDomain,
|
||||||
selectFirstAvailableTimeSlotNextMonth,
|
selectFirstAvailableTimeSlotNextMonth,
|
||||||
selectSecondAvailableTimeSlotNextMonth,
|
selectSecondAvailableTimeSlotNextMonth,
|
||||||
@@ -98,6 +99,58 @@ test("dynamic booking info prefilled by query params", async ({ page, users }) =
|
|||||||
activeState = await listItemLocator.getAttribute("data-active");
|
activeState = await listItemLocator.getAttribute("data-active");
|
||||||
expect(activeState).toEqual("false");
|
expect(activeState).toEqual("false");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("multiple duration selection updates event length correctly", async ({ page, users }) => {
|
||||||
|
const user = await users.create();
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await test.step("update event title to include duration placeholder", async () => {
|
||||||
|
await page.goto("/event-types");
|
||||||
|
await page.waitForSelector('[data-testid="event-types"]');
|
||||||
|
await page.click(`text=Multiple duration`);
|
||||||
|
await page.waitForSelector('[data-testid="event-title"]');
|
||||||
|
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
|
||||||
|
"aria-current",
|
||||||
|
"page"
|
||||||
|
);
|
||||||
|
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
|
||||||
|
await page.fill('[name="eventName"]', "{Event duration} event btwn {Organiser} {Scheduler}");
|
||||||
|
await page.locator('[data-testid="update-eventtype"]').click();
|
||||||
|
await page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/${user.username}/multiple-duration`);
|
||||||
|
|
||||||
|
await page.locator('[data-testid="multiple-choice-30mins"]').waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
await test.step("verify default 30min duration is selected", async () => {
|
||||||
|
const duration30 = page.getByTestId("multiple-choice-30mins");
|
||||||
|
const activeState = await duration30.getAttribute("data-active");
|
||||||
|
expect(activeState).toEqual("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("book with 90min duration and verify title", async () => {
|
||||||
|
await page.getByTestId("multiple-choice-90mins").click();
|
||||||
|
|
||||||
|
const duration90 = page.getByTestId("multiple-choice-90mins");
|
||||||
|
const activeState = await duration90.getAttribute("data-active");
|
||||||
|
expect(activeState).toEqual("true");
|
||||||
|
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
|
||||||
|
await page.fill('[name="name"]', "Test User");
|
||||||
|
await page.fill('[name="email"]', "test@example.com");
|
||||||
|
await confirmBooking(page);
|
||||||
|
|
||||||
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
|
const bookingTitle = page.locator('[data-testid="booking-title"]');
|
||||||
|
await expect(bookingTitle).toBeVisible();
|
||||||
|
const titleText = await bookingTitle.textContent();
|
||||||
|
expect(titleText).toContain("90");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line playwright/no-skipped-test
|
// eslint-disable-next-line playwright/no-skipped-test
|
||||||
test.skip("it contains the right event details", async ({ page }) => {
|
test.skip("it contains the right event details", async ({ page }) => {
|
||||||
const response = await page.goto(`http://acme.cal.local:3000/owner1+member1`);
|
const response = await page.goto(`http://acme.cal.local:3000/owner1+member1`);
|
||||||
|
|||||||
+477
-199
@@ -4193,6 +4193,284 @@
|
|||||||
"tags": ["Orgs / Teams / Event Types"]
|
"tags": ["Orgs / Teams / Event Types"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_createPrivateLink",
|
||||||
|
"summary": "Create a private link for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreatePrivateLinkInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreatePrivateLinkOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks",
|
||||||
|
"summary": "Get all private links for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/GetPrivateLinksOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": {
|
||||||
|
"patch": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_updatePrivateLink",
|
||||||
|
"summary": "Update a private link for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linkId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdatePrivateLinkOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink",
|
||||||
|
"summary": "Delete a private link for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linkId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/DeletePrivateLinkOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
|
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
|
||||||
@@ -21786,6 +22064,188 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CreatePrivateLinkInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Expiration date for time-based links",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2024-12-31T23:59:59.000Z"
|
||||||
|
},
|
||||||
|
"maxUsageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
||||||
|
"example": 10,
|
||||||
|
"minimum": 1,
|
||||||
|
"default": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TimeBasedPrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"linkId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The private link ID",
|
||||||
|
"example": "abc123def456"
|
||||||
|
},
|
||||||
|
"eventTypeId": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Event type ID this link belongs to",
|
||||||
|
"example": 123
|
||||||
|
},
|
||||||
|
"isExpired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the link is currently expired",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"bookingUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Full booking URL for this private link",
|
||||||
|
"format": "uri",
|
||||||
|
"example": "https://cal.com/d/abc123def456/30min"
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Expiration date for this time-based link",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2025-12-31T23:59:59.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "expiresAt"]
|
||||||
|
},
|
||||||
|
"UsageBasedPrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"linkId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The private link ID",
|
||||||
|
"example": "abc123def456"
|
||||||
|
},
|
||||||
|
"eventTypeId": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Event type ID this link belongs to",
|
||||||
|
"example": 123
|
||||||
|
},
|
||||||
|
"isExpired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the link is currently expired",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"bookingUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Full booking URL for this private link",
|
||||||
|
"format": "uri",
|
||||||
|
"example": "https://cal.com/d/abc123def456/30min"
|
||||||
|
},
|
||||||
|
"maxUsageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum number of times this link can be used",
|
||||||
|
"example": 10
|
||||||
|
},
|
||||||
|
"usageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Current usage count for this link",
|
||||||
|
"example": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "maxUsageCount", "usageCount"]
|
||||||
|
},
|
||||||
|
"CreatePrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"description": "Created private link data (either time-based or usage-based)",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["status", "data"]
|
||||||
|
},
|
||||||
|
"GetPrivateLinksOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of private links for the event type (mix of time-based and usage-based)",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["status", "data"]
|
||||||
|
},
|
||||||
|
"UpdatePrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"description": "Updated private link data (either time-based or usage-based)",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["status", "data"]
|
||||||
|
},
|
||||||
|
"DeletePrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Deleted link information",
|
||||||
|
"properties": {
|
||||||
|
"linkId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "abc123def456"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Private link deleted successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["status", "data"]
|
||||||
|
},
|
||||||
"StripConnectOutputDto": {
|
"StripConnectOutputDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -23368,6 +23828,23 @@
|
|||||||
},
|
},
|
||||||
"required": ["status", "data"]
|
"required": ["status", "data"]
|
||||||
},
|
},
|
||||||
|
"UpdatePrivateLinkBody": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
"description": "New expiration date for time-based links",
|
||||||
|
"example": "2024-12-31T23:59:59.000Z"
|
||||||
|
},
|
||||||
|
"maxUsageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "New maximum number of times the link can be used",
|
||||||
|
"example": 10,
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"MeOrgOutput": {
|
"MeOrgOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -25757,205 +26234,6 @@
|
|||||||
},
|
},
|
||||||
"required": ["status", "data"]
|
"required": ["status", "data"]
|
||||||
},
|
},
|
||||||
"CreatePrivateLinkInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"expiresAt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Expiration date for time-based links",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2024-12-31T23:59:59.000Z"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
|
||||||
"example": 10,
|
|
||||||
"minimum": 1,
|
|
||||||
"default": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TimeBasedPrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The private link ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"eventTypeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Event type ID this link belongs to",
|
|
||||||
"example": 123
|
|
||||||
},
|
|
||||||
"isExpired": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the link is currently expired",
|
|
||||||
"example": false
|
|
||||||
},
|
|
||||||
"bookingUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full booking URL for this private link",
|
|
||||||
"format": "uri",
|
|
||||||
"example": "https://cal.com/d/abc123def456/30min"
|
|
||||||
},
|
|
||||||
"expiresAt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Expiration date for this time-based link",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2025-12-31T23:59:59.000Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "expiresAt"]
|
|
||||||
},
|
|
||||||
"UsageBasedPrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The private link ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"eventTypeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Event type ID this link belongs to",
|
|
||||||
"example": 123
|
|
||||||
},
|
|
||||||
"isExpired": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the link is currently expired",
|
|
||||||
"example": false
|
|
||||||
},
|
|
||||||
"bookingUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full booking URL for this private link",
|
|
||||||
"format": "uri",
|
|
||||||
"example": "https://cal.com/d/abc123def456/30min"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of times this link can be used",
|
|
||||||
"example": 10
|
|
||||||
},
|
|
||||||
"usageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Current usage count for this link",
|
|
||||||
"example": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "maxUsageCount", "usageCount"]
|
|
||||||
},
|
|
||||||
"CreatePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Created private link data (either time-based or usage-based)",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"GetPrivateLinksOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Array of private links for the event type (mix of time-based and usage-based)",
|
|
||||||
"items": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"UpdatePrivateLinkBody": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"expiresAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string",
|
|
||||||
"description": "New expiration date for time-based links",
|
|
||||||
"example": "2024-12-31T23:59:59.000Z"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "New maximum number of times the link can be used",
|
|
||||||
"example": 10,
|
|
||||||
"minimum": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"UpdatePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Updated private link data (either time-based or usage-based)",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"DeletePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Deleted link information",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Private link deleted successfully"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"UserWebhookOutputDto": {
|
"UserWebhookOutputDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ Below is a list of props that can be passed to the booker atom.
|
|||||||
| onTimeslotsLoaded | No | Callback function triggered once the available timeslots have been fetched. |
|
| onTimeslotsLoaded | No | Callback function triggered once the available timeslots have been fetched. |
|
||||||
| roundRobinHideOrgAndTeam | No | Boolean indicating if the organization and team should be hidden in the booker atom sidebar for round robin scheduling type, defaults to false. |
|
| roundRobinHideOrgAndTeam | No | Boolean indicating if the organization and team should be hidden in the booker atom sidebar for round robin scheduling type, defaults to false. |
|
||||||
| showNoAvailabilityDialog | No | Boolean indicating if the no availability dialog should be shown, defaults to true. |
|
| showNoAvailabilityDialog | No | Boolean indicating if the no availability dialog should be shown, defaults to true. |
|
||||||
|
| silentlyHandleCalendarFailures | No | Boolean when true the booker still displays slots when the third party calendars credentials are invalid or expired, Booker may show stale availability when enabled |
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ Sometimes, yarn install might fail during deployment on Vercel, in which case, y
|
|||||||
<Step title="Override the build command">
|
<Step title="Override the build command">
|
||||||
Override to:
|
Override to:
|
||||||
```
|
```
|
||||||
cd ../.. && yarn build --include-dependencies --no-deps
|
cd ../.. && yarn build --no-deps
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Hit Deploy">
|
<Step title="Hit Deploy">
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ RUN set -eux; \
|
|||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
|
||||||
yarn config set httpTimeout 1200000 && \
|
yarn config set httpTimeout 1200000 && \
|
||||||
npx turbo prune --scope=@calcom/web --docker && \
|
npx turbo prune --filter=@calcom/web --docker && \
|
||||||
npx turbo prune --scope=@calcom/api --docker && \
|
npx turbo prune --filter=@calcom/api --docker && \
|
||||||
yarn install && \
|
yarn install && \
|
||||||
yarn turbo run build --filter=@calcom/api
|
yarn turbo run build --filter=@calcom/api
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -28,23 +28,23 @@
|
|||||||
"edit-app-template": "yarn app-store edit-template",
|
"edit-app-template": "yarn app-store edit-template",
|
||||||
"delete-app-template": "yarn app-store delete-template",
|
"delete-app-template": "yarn app-store delete-template",
|
||||||
"build": "turbo run build --filter=@calcom/web...",
|
"build": "turbo run build --filter=@calcom/web...",
|
||||||
"build:ai": "turbo run build --scope=\"@calcom/ai\"",
|
"build:ai": "turbo run build --filter=\"@calcom/ai\"",
|
||||||
"clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf",
|
"clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf",
|
||||||
"db-deploy": "turbo run db-deploy",
|
"db-deploy": "turbo run db-deploy",
|
||||||
"db-seed": "turbo run db-seed",
|
"db-seed": "turbo run db-seed",
|
||||||
"db-studio": "yarn prisma studio",
|
"db-studio": "yarn prisma studio",
|
||||||
"deploy": "turbo run deploy",
|
"deploy": "turbo run deploy",
|
||||||
"dev:all": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\" --scope=\"@calcom/console\"",
|
"dev:all": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\" --filter=\"@calcom/console\"",
|
||||||
"dev:ai": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/ai\"",
|
"dev:ai": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/ai\"",
|
||||||
"dev:api": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\"",
|
"dev:api": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\"",
|
||||||
"dev:api:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/console\"",
|
"dev:api:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/console\"",
|
||||||
"dev:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/console\"",
|
"dev:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/console\"",
|
||||||
"dev:swagger": "turbo run dev --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/swagger\"",
|
"dev:swagger": "turbo run dev --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/swagger\"",
|
||||||
"dev:website": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\"",
|
"dev:website": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\"",
|
||||||
"dev": "turbo run dev --scope=\"@calcom/web\"",
|
"dev": "turbo run dev --filter=\"@calcom/web\"",
|
||||||
"build-storybook": "turbo run build --scope=\"@calcom/storybook\"",
|
"build-storybook": "turbo run build --filter=\"@calcom/storybook\"",
|
||||||
"dx": "turbo run dx",
|
"dx": "turbo run dx",
|
||||||
"i-dev": "infisical run -- turbo run dev --scope=\"@calcom/web\"",
|
"i-dev": "infisical run -- turbo run dev --filter=\"@calcom/web\"",
|
||||||
"i-dx": "infisical run -- turbo run dx",
|
"i-dx": "infisical run -- turbo run dx",
|
||||||
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
|
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
|
||||||
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
|
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
"pre-commit": "lint-staged",
|
"pre-commit": "lint-staged",
|
||||||
"predev": "echo 'Checking env files'",
|
"predev": "echo 'Checking env files'",
|
||||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||||
"start": "turbo run start --scope=\"@calcom/web\"",
|
"start": "turbo run start --filter=\"@calcom/web\"",
|
||||||
"tdd": "vitest watch",
|
"tdd": "vitest watch",
|
||||||
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
|
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
|
||||||
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
|
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"turbo": "^1.10.1"
|
"turbo": "^2.5.5"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"types-ramda": "0.29.4",
|
"types-ramda": "0.29.4",
|
||||||
|
|||||||
@@ -9,6 +9,21 @@ function _fireEvent(fullName: string, detail: CustomEventDetail) {
|
|||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BaseBookingEventPayload = {
|
||||||
|
title: string | undefined;
|
||||||
|
startTime: string | undefined;
|
||||||
|
endTime: string | undefined;
|
||||||
|
eventTypeId: number | null | undefined;
|
||||||
|
status: string | undefined;
|
||||||
|
paymentRequired: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
/**
|
||||||
|
* This is only used for recurring bookings
|
||||||
|
*/
|
||||||
|
allBookings?: { startTime: string; endTime: string }[];
|
||||||
|
videoCallUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type EventDataMap = {
|
export type EventDataMap = {
|
||||||
eventTypeSelected: {
|
eventTypeSelected: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -24,20 +39,8 @@ export type EventDataMap = {
|
|||||||
linkReady: Record<string, never>;
|
linkReady: Record<string, never>;
|
||||||
__connectInitiated: Record<string, never>;
|
__connectInitiated: Record<string, never>;
|
||||||
__connectCompleted: Record<string, never>;
|
__connectCompleted: Record<string, never>;
|
||||||
bookingSuccessfulV2: {
|
bookingSuccessfulV2: BaseBookingEventPayload & {
|
||||||
uid: string | undefined;
|
uid: string | undefined;
|
||||||
title: string | undefined;
|
|
||||||
startTime: string | undefined;
|
|
||||||
endTime: string | undefined;
|
|
||||||
eventTypeId: number | null | undefined;
|
|
||||||
status: string | undefined;
|
|
||||||
paymentRequired: boolean;
|
|
||||||
isRecurring: boolean;
|
|
||||||
/**
|
|
||||||
* This is only used for recurring bookings
|
|
||||||
*/
|
|
||||||
allBookings?: { startTime: string; endTime: string }[];
|
|
||||||
videoCallUrl?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,20 +59,11 @@ export type EventDataMap = {
|
|||||||
};
|
};
|
||||||
confirmed: boolean;
|
confirmed: boolean;
|
||||||
};
|
};
|
||||||
rescheduleBookingSuccessfulV2: {
|
rescheduleBookingSuccessfulV2: BaseBookingEventPayload & {
|
||||||
uid: string | undefined;
|
uid: string | undefined;
|
||||||
title: string | undefined;
|
|
||||||
startTime: string | undefined;
|
|
||||||
endTime: string | undefined;
|
|
||||||
eventTypeId: number | null | undefined;
|
|
||||||
status: string | undefined;
|
|
||||||
paymentRequired: boolean;
|
|
||||||
isRecurring: boolean;
|
|
||||||
/**
|
|
||||||
* This is only used for recurring bookings
|
|
||||||
*/
|
|
||||||
allBookings?: { startTime: string; endTime: string }[];
|
|
||||||
};
|
};
|
||||||
|
dryRunBookingSuccessfulV2: BaseBookingEventPayload;
|
||||||
|
dryRunRescheduleBookingSuccessfulV2: BaseBookingEventPayload;
|
||||||
/**
|
/**
|
||||||
* @deprecated Use `rescheduleBookingSuccessfulV2` instead. We restrict the data heavily there, only sending what is absolutely needed and keeping it light as well. Plus, more importantly that can be documented well.
|
* @deprecated Use `rescheduleBookingSuccessfulV2` instead. We restrict the data heavily there, only sending what is absolutely needed and keeping it light as well. Plus, more importantly that can be documented well.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { parse } from "accept-language-parser";
|
|||||||
import { lookup } from "bcp-47-match";
|
import { lookup } from "bcp-47-match";
|
||||||
import type { GetTokenParams } from "next-auth/jwt";
|
import type { GetTokenParams } from "next-auth/jwt";
|
||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
import { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
|
||||||
import { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
|
||||||
|
|
||||||
import { i18n } from "@calcom/config/next-i18next.config";
|
import { i18n } from "@calcom/config/next-i18next.config";
|
||||||
|
|
||||||
|
type ReadonlyHeaders = Awaited<ReturnType<typeof import("next/headers").headers>>;
|
||||||
|
type ReadonlyRequestCookies = Awaited<ReturnType<typeof import("next/headers").cookies>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a slimmed down version of the `getServerSession` function from
|
* This is a slimmed down version of the `getServerSession` function from
|
||||||
* `next-auth`.
|
* `next-auth`.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import dayjs from "@calcom/dayjs";
|
|||||||
import PoweredBy from "@calcom/ee/components/PoweredBy";
|
import PoweredBy from "@calcom/ee/components/PoweredBy";
|
||||||
import { updateEmbedBookerState } from "@calcom/embed-core/src/embed-iframe";
|
import { updateEmbedBookerState } from "@calcom/embed-core/src/embed-iframe";
|
||||||
import TurnstileCaptcha from "@calcom/features/auth/Turnstile";
|
import TurnstileCaptcha from "@calcom/features/auth/Turnstile";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import useSkipConfirmStep from "@calcom/features/bookings/Booker/components/hooks/useSkipConfirmStep";
|
import useSkipConfirmStep from "@calcom/features/bookings/Booker/components/hooks/useSkipConfirmStep";
|
||||||
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
||||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
|
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
|
||||||
@@ -39,7 +40,6 @@ import { NotFound } from "./components/Unavailable";
|
|||||||
import { useIsQuickAvailabilityCheckFeatureEnabled } from "./components/hooks/useIsQuickAvailabilityCheckFeatureEnabled";
|
import { useIsQuickAvailabilityCheckFeatureEnabled } from "./components/hooks/useIsQuickAvailabilityCheckFeatureEnabled";
|
||||||
import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
|
import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
|
||||||
import framerFeatures from "./framer-features";
|
import framerFeatures from "./framer-features";
|
||||||
import { useBookerStore } from "./store";
|
|
||||||
import type { BookerProps, WrappedBookerProps } from "./types";
|
import type { BookerProps, WrappedBookerProps } from "./types";
|
||||||
import { isBookingDryRun } from "./utils/isBookingDryRun";
|
import { isBookingDryRun } from "./utils/isBookingDryRun";
|
||||||
import { isTimeSlotAvailable } from "./utils/isTimeslotAvailable";
|
import { isTimeSlotAvailable } from "./utils/isTimeslotAvailable";
|
||||||
@@ -85,9 +85,14 @@ const BookerComponent = ({
|
|||||||
}: BookerProps & WrappedBookerProps) => {
|
}: BookerProps & WrappedBookerProps) => {
|
||||||
const searchParams = useCompatSearchParams();
|
const searchParams = useCompatSearchParams();
|
||||||
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
|
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
|
||||||
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
|
const [bookerState, setBookerState] = useBookerStoreContext(
|
||||||
|
(state) => [state.state, state.setState],
|
||||||
|
shallow
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
|
||||||
|
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
|
||||||
|
|
||||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
|
||||||
const {
|
const {
|
||||||
shouldShowFormInDialog,
|
shouldShowFormInDialog,
|
||||||
hasDarkBackground,
|
hasDarkBackground,
|
||||||
@@ -100,12 +105,15 @@ const BookerComponent = ({
|
|||||||
bookerLayouts,
|
bookerLayouts,
|
||||||
} = bookerLayout;
|
} = bookerLayout;
|
||||||
|
|
||||||
const [seatedEventData, setSeatedEventData] = useBookerStore(
|
const [seatedEventData, setSeatedEventData] = useBookerStoreContext(
|
||||||
(state) => [state.seatedEventData, state.setSeatedEventData],
|
(state) => [state.seatedEventData, state.setSeatedEventData],
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
const { selectedTimeslot, setSelectedTimeslot, allSelectedTimeslots } = slots;
|
const { selectedTimeslot, setSelectedTimeslot, allSelectedTimeslots } = slots;
|
||||||
const [dayCount, setDayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
|
const [dayCount, setDayCount] = useBookerStoreContext(
|
||||||
|
(state) => [state.dayCount, state.setDayCount],
|
||||||
|
shallow
|
||||||
|
);
|
||||||
|
|
||||||
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter(
|
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter(
|
||||||
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
|
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
|
||||||
@@ -184,6 +192,10 @@ const BookerComponent = ({
|
|||||||
(bookerState === "booking" || (bookerState === "selecting_time" && skipConfirmStep))
|
(bookerState === "booking" || (bookerState === "selecting_time" && skipConfirmStep))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onAvailableTimeSlotSelect = (time: string) => {
|
||||||
|
setSelectedTimeslot(time);
|
||||||
|
};
|
||||||
|
|
||||||
updateEmbedBookerState({ bookerState, slotsQuery: schedule });
|
updateEmbedBookerState({ bookerState, slotsQuery: schedule });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -220,6 +232,7 @@ const BookerComponent = ({
|
|||||||
return bookerState === "booking" ? (
|
return bookerState === "booking" ? (
|
||||||
<BookEventForm
|
<BookEventForm
|
||||||
key={key}
|
key={key}
|
||||||
|
timeslot={selectedTimeslot}
|
||||||
shouldRenderCaptcha={shouldRenderCaptcha}
|
shouldRenderCaptcha={shouldRenderCaptcha}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setSelectedTimeslot(null);
|
setSelectedTimeslot(null);
|
||||||
@@ -312,9 +325,7 @@ const BookerComponent = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{event.data && !isPlatform ? <BookingPageTagManager eventType={event.data} /> : <></>}
|
{event.data && !isPlatform ? <BookingPageTagManager eventType={event.data} /> : <></>}
|
||||||
|
|
||||||
{(isBookingDryRunProp || isBookingDryRun(searchParams)) && <DryRunMessage isEmbed={isEmbed} />}
|
{(isBookingDryRunProp || isBookingDryRun(searchParams)) && <DryRunMessage isEmbed={isEmbed} />}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
// In a popup embed, if someone clicks outside the main(having main class or main tag), it closes the embed
|
// In a popup embed, if someone clicks outside the main(having main class or main tag), it closes the embed
|
||||||
@@ -394,6 +405,7 @@ const BookerComponent = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<EventMeta
|
<EventMeta
|
||||||
|
selectedTimeslot={selectedTimeslot}
|
||||||
classNames={{
|
classNames={{
|
||||||
eventMetaContainer: customClassNames?.eventMetaCustomClassNames?.eventMetaContainer,
|
eventMetaContainer: customClassNames?.eventMetaCustomClassNames?.eventMetaContainer,
|
||||||
eventMetaTitle: customClassNames?.eventMetaCustomClassNames?.eventMetaTitle,
|
eventMetaTitle: customClassNames?.eventMetaCustomClassNames?.eventMetaTitle,
|
||||||
@@ -480,6 +492,7 @@ const BookerComponent = ({
|
|||||||
ref={timeslotsRef}
|
ref={timeslotsRef}
|
||||||
{...fadeInLeft}>
|
{...fadeInLeft}>
|
||||||
<AvailableTimeSlots
|
<AvailableTimeSlots
|
||||||
|
onAvailableTimeSlotSelect={onAvailableTimeSlotSelect}
|
||||||
customClassNames={customClassNames?.availableTimeSlotsCustomClassNames}
|
customClassNames={customClassNames?.availableTimeSlotsCustomClassNames}
|
||||||
extraDays={extraDays}
|
extraDays={extraDays}
|
||||||
limitHeight={layout === BookerLayouts.MONTH_VIEW}
|
limitHeight={layout === BookerLayouts.MONTH_VIEW}
|
||||||
@@ -552,7 +565,6 @@ const BookerComponent = ({
|
|||||||
</m.span>
|
</m.span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<>
|
<>
|
||||||
{verifyCode && formEmail ? (
|
{verifyCode && formEmail ? (
|
||||||
<VerifyCodeDialog
|
<VerifyCodeDialog
|
||||||
@@ -571,7 +583,6 @@ const BookerComponent = ({
|
|||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
||||||
<BookFormAsModal
|
<BookFormAsModal
|
||||||
onCancel={() => setSelectedTimeslot(null)}
|
onCancel={() => setSelectedTimeslot(null)}
|
||||||
visible={bookerState === "booking" && shouldShowFormInDialog}>
|
visible={bookerState === "booking" && shouldShowFormInDialog}>
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext, useRef, type ReactNode, useEffect } from "react";
|
||||||
|
import { useStore } from "zustand";
|
||||||
|
import type { StoreApi } from "zustand";
|
||||||
|
|
||||||
|
import { createBookerStore, type BookerStore, type StoreInitializeType } from "./store";
|
||||||
|
|
||||||
|
export const BookerStoreContext = createContext<StoreApi<BookerStore> | null>(null);
|
||||||
|
|
||||||
|
export interface BookerStoreProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BookerStoreProvider = ({ children }: BookerStoreProviderProps) => {
|
||||||
|
const storeRef = useRef<StoreApi<BookerStore>>();
|
||||||
|
if (!storeRef.current) {
|
||||||
|
storeRef.current = createBookerStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BookerStoreContext.Provider value={storeRef.current}>{children}</BookerStoreContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBookerStoreContext = <T,>(
|
||||||
|
selector: (store: BookerStore) => T,
|
||||||
|
equalityFn?: (a: T, b: T) => boolean
|
||||||
|
): T => {
|
||||||
|
const bookerStoreContext = useContext(BookerStoreContext);
|
||||||
|
|
||||||
|
if (!bookerStoreContext) {
|
||||||
|
throw new Error("useBookerStoreContext must be used within BookerStoreProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return useStore(bookerStoreContext, selector, equalityFn);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useInitializeBookerStoreContext = ({
|
||||||
|
username,
|
||||||
|
eventSlug,
|
||||||
|
month,
|
||||||
|
eventId,
|
||||||
|
rescheduleUid = null,
|
||||||
|
rescheduledBy = null,
|
||||||
|
bookingData = null,
|
||||||
|
verifiedEmail = null,
|
||||||
|
layout,
|
||||||
|
isTeamEvent,
|
||||||
|
durationConfig,
|
||||||
|
org,
|
||||||
|
isInstantMeeting,
|
||||||
|
timezone = null,
|
||||||
|
teamMemberEmail,
|
||||||
|
crmOwnerRecordType,
|
||||||
|
crmAppSlug,
|
||||||
|
crmRecordId,
|
||||||
|
isPlatform = false,
|
||||||
|
allowUpdatingUrlParams = true,
|
||||||
|
}: StoreInitializeType) => {
|
||||||
|
const bookerStoreContext = useContext(BookerStoreContext);
|
||||||
|
|
||||||
|
if (!bookerStoreContext) {
|
||||||
|
throw new Error("useInitializeBookerStoreContext must be used within BookerStoreProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeStore = useStore(bookerStoreContext, (state) => state.initialize);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeStore({
|
||||||
|
username,
|
||||||
|
eventSlug,
|
||||||
|
month,
|
||||||
|
eventId,
|
||||||
|
rescheduleUid,
|
||||||
|
rescheduledBy,
|
||||||
|
bookingData,
|
||||||
|
layout,
|
||||||
|
isTeamEvent,
|
||||||
|
org,
|
||||||
|
verifiedEmail,
|
||||||
|
durationConfig,
|
||||||
|
isInstantMeeting,
|
||||||
|
timezone,
|
||||||
|
teamMemberEmail,
|
||||||
|
crmOwnerRecordType,
|
||||||
|
crmAppSlug,
|
||||||
|
crmRecordId,
|
||||||
|
isPlatform,
|
||||||
|
allowUpdatingUrlParams,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
initializeStore,
|
||||||
|
org,
|
||||||
|
username,
|
||||||
|
eventSlug,
|
||||||
|
month,
|
||||||
|
eventId,
|
||||||
|
rescheduleUid,
|
||||||
|
rescheduledBy,
|
||||||
|
bookingData,
|
||||||
|
layout,
|
||||||
|
isTeamEvent,
|
||||||
|
verifiedEmail,
|
||||||
|
durationConfig,
|
||||||
|
isInstantMeeting,
|
||||||
|
timezone,
|
||||||
|
teamMemberEmail,
|
||||||
|
crmOwnerRecordType,
|
||||||
|
crmAppSlug,
|
||||||
|
crmRecordId,
|
||||||
|
isPlatform,
|
||||||
|
allowUpdatingUrlParams,
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -10,18 +10,17 @@ import "@calcom/features/bookings/Booker/components/__mocks__/Section";
|
|||||||
import { constantsScenarios } from "@calcom/lib/__mocks__/constants";
|
import { constantsScenarios } from "@calcom/lib/__mocks__/constants";
|
||||||
import "@calcom/lib/__mocks__/logger";
|
import "@calcom/lib/__mocks__/logger";
|
||||||
|
|
||||||
import { render, screen } from "@testing-library/react";
|
import React from "react";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
import "@calcom/dayjs/__mocks__";
|
import "@calcom/dayjs/__mocks__";
|
||||||
import "@calcom/features/auth/Turnstile";
|
import "@calcom/features/auth/Turnstile";
|
||||||
|
|
||||||
import { Booker } from "../Booker";
|
import { Booker } from "../Booker";
|
||||||
import { useBookerStore } from "../store";
|
import { render, screen } from "./test-utils";
|
||||||
import type { BookerState } from "../types";
|
|
||||||
|
|
||||||
vi.mock("framer-motion", async (importOriginal) => {
|
vi.mock("framer-motion", async (importOriginal) => {
|
||||||
const actual = await importOriginal();
|
const actual = (await importOriginal()) as any;
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
};
|
};
|
||||||
@@ -87,27 +86,6 @@ vi.mock("@calcom/atoms/hooks/useIsPlatform", () => ({
|
|||||||
useIsPlatform: () => false,
|
useIsPlatform: () => false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update mockStoreState to include all required state
|
|
||||||
const mockStoreState = {
|
|
||||||
state: "booking" as BookerState,
|
|
||||||
setState: vi.fn(),
|
|
||||||
selectedDate: "2024-01-01",
|
|
||||||
seatedEventData: {},
|
|
||||||
setSeatedEventData: vi.fn(),
|
|
||||||
tentativeSelectedTimeslots: [],
|
|
||||||
setTentativeSelectedTimeslots: vi.fn(),
|
|
||||||
dayCount: 7,
|
|
||||||
setDayCount: vi.fn(),
|
|
||||||
setSelectedTimeslot: vi.fn(),
|
|
||||||
selectedTimeslot: null,
|
|
||||||
formStep: 0,
|
|
||||||
setFormStep: vi.fn(),
|
|
||||||
bookerState: "booking",
|
|
||||||
setBookerState: vi.fn(),
|
|
||||||
layout: "default",
|
|
||||||
setLayout: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update defaultProps to include missing required props
|
// Update defaultProps to include missing required props
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
username: "testuser",
|
username: "testuser",
|
||||||
@@ -188,7 +166,7 @@ const defaultProps = {
|
|||||||
describe("Booker", () => {
|
describe("Booker", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
constantsScenarios.set({
|
constantsScenarios.set({
|
||||||
PUBLIC_QUICK_AVAILABILITY_ROLLOUT: 100,
|
PUBLIC_QUICK_AVAILABILITY_ROLLOUT: "100",
|
||||||
POWERED_BY_URL: "https://go.cal.com/booking",
|
POWERED_BY_URL: "https://go.cal.com/booking",
|
||||||
APP_NAME: "Cal.com",
|
APP_NAME: "Cal.com",
|
||||||
});
|
});
|
||||||
@@ -196,24 +174,13 @@ describe("Booker", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should render null when in loading state", () => {
|
it("should render null when in loading state", () => {
|
||||||
useBookerStore.setState({
|
const { container } = render(<Booker {...defaultProps} />, {
|
||||||
...mockStoreState,
|
mockStore: { state: "loading" },
|
||||||
state: "loading",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { container } = render(<Booker {...defaultProps} />);
|
|
||||||
expect(container).toBeEmptyDOMElement();
|
expect(container).toBeEmptyDOMElement();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render DryRunMessage when in dry run mode", () => {
|
it("should render DryRunMessage when in dry run mode", () => {
|
||||||
useBookerStore.setState({
|
|
||||||
...mockStoreState,
|
|
||||||
state: "selecting_time",
|
|
||||||
selectedDate: "2024-01-01",
|
|
||||||
selectedTimeslot: "2024-01-01T10:00:00Z",
|
|
||||||
tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const propsWithDryRun = {
|
const propsWithDryRun = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
isBookingDryRun: true,
|
isBookingDryRun: true,
|
||||||
@@ -226,7 +193,14 @@ describe("Booker", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<Booker {...propsWithDryRun} />);
|
render(<Booker {...propsWithDryRun} />, {
|
||||||
|
mockStore: {
|
||||||
|
state: "selecting_time",
|
||||||
|
selectedDate: "2024-01-01",
|
||||||
|
selectedTimeslot: "2024-01-01T10:00:00Z",
|
||||||
|
tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
|
||||||
|
},
|
||||||
|
});
|
||||||
expect(screen.getByTestId("dry-run-message")).toBeInTheDocument();
|
expect(screen.getByTestId("dry-run-message")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -242,12 +216,10 @@ describe("Booker", () => {
|
|||||||
invalidate: mockInvalidate,
|
invalidate: mockInvalidate,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
useBookerStore.setState({
|
|
||||||
...mockStoreState,
|
|
||||||
state: "booking",
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<Booker {...propsWithInvalidate} />);
|
render(<Booker {...propsWithInvalidate} />, {
|
||||||
|
mockStore: { state: "booking" },
|
||||||
|
});
|
||||||
screen.logTestingPlaygroundURL();
|
screen.logTestingPlaygroundURL();
|
||||||
// Trigger form cancel
|
// Trigger form cancel
|
||||||
const cancelButton = screen.getByRole("button", { name: /cancel/i });
|
const cancelButton = screen.getByRole("button", { name: /cancel/i });
|
||||||
@@ -267,12 +239,9 @@ describe("Booker", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
useBookerStore.setState({
|
render(<Booker {...propsWithQuickChecks} />, {
|
||||||
...mockStoreState,
|
mockStore: { state: "booking" },
|
||||||
state: "booking",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<Booker {...propsWithQuickChecks} />);
|
|
||||||
const bookEventForm = screen.getByTestId("book-event-form");
|
const bookEventForm = screen.getByTestId("book-event-form");
|
||||||
await expect(bookEventForm).toHaveAttribute("data-unavailable", "true");
|
await expect(bookEventForm).toHaveAttribute("data-unavailable", "true");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import type { RenderOptions } from "@testing-library/react";
|
||||||
|
import React from "react";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
import type { StoreApi } from "zustand";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
import { BookerStoreContext } from "../BookerStoreProvider";
|
||||||
|
import type { BookerStore } from "../store";
|
||||||
|
|
||||||
|
interface CustomRenderOptions extends Omit<RenderOptions, "wrapper"> {
|
||||||
|
mockStore?: Partial<BookerStore>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMockStore = (initialState?: Partial<BookerStore>): StoreApi<BookerStore> => {
|
||||||
|
let state: BookerStore = {
|
||||||
|
username: null,
|
||||||
|
eventSlug: null,
|
||||||
|
eventId: null,
|
||||||
|
verifiedEmail: null,
|
||||||
|
setVerifiedEmail: vi.fn(),
|
||||||
|
month: dayjs().format("YYYY-MM"),
|
||||||
|
setMonth: vi.fn(),
|
||||||
|
state: "loading",
|
||||||
|
setState: vi.fn(),
|
||||||
|
layout: BookerLayouts.MONTH_VIEW,
|
||||||
|
setLayout: vi.fn(),
|
||||||
|
selectedDate: null,
|
||||||
|
setSelectedDate: vi.fn(),
|
||||||
|
addToSelectedDate: vi.fn(),
|
||||||
|
selectedDatesAndTimes: null,
|
||||||
|
setSelectedDatesAndTimes: vi.fn(),
|
||||||
|
durationConfig: null,
|
||||||
|
selectedDuration: null,
|
||||||
|
setSelectedDuration: vi.fn(),
|
||||||
|
selectedTimeslot: null,
|
||||||
|
setSelectedTimeslot: vi.fn(),
|
||||||
|
tentativeSelectedTimeslots: [],
|
||||||
|
setTentativeSelectedTimeslots: vi.fn(),
|
||||||
|
recurringEventCount: null,
|
||||||
|
setRecurringEventCount: vi.fn(),
|
||||||
|
occurenceCount: null,
|
||||||
|
setOccurenceCount: vi.fn(),
|
||||||
|
dayCount: null,
|
||||||
|
setDayCount: vi.fn(),
|
||||||
|
rescheduleUid: null,
|
||||||
|
rescheduledBy: null,
|
||||||
|
bookingUid: null,
|
||||||
|
bookingData: null,
|
||||||
|
setBookingData: vi.fn(),
|
||||||
|
initialize: vi.fn(),
|
||||||
|
formValues: {},
|
||||||
|
setFormValues: vi.fn(),
|
||||||
|
isTeamEvent: false,
|
||||||
|
seatedEventData: {},
|
||||||
|
setSeatedEventData: vi.fn(),
|
||||||
|
isInstantMeeting: false,
|
||||||
|
org: null,
|
||||||
|
setOrg: vi.fn(),
|
||||||
|
timezone: null,
|
||||||
|
setTimezone: vi.fn(),
|
||||||
|
teamMemberEmail: null,
|
||||||
|
crmOwnerRecordType: null,
|
||||||
|
crmAppSlug: null,
|
||||||
|
crmRecordId: null,
|
||||||
|
isPlatform: false,
|
||||||
|
allowUpdatingUrlParams: true,
|
||||||
|
...initialState,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.setMonth = vi.fn((month: string | null) => {
|
||||||
|
state.month = month;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getState: () => state,
|
||||||
|
setState: vi.fn((updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
const newState = updater(state);
|
||||||
|
state = { ...state, ...newState };
|
||||||
|
} else {
|
||||||
|
state = { ...state, ...updater };
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
subscribe: vi.fn(),
|
||||||
|
destroy: vi.fn(),
|
||||||
|
} as unknown as StoreApi<BookerStore>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderWithBookerStore = (ui: ReactElement, options?: CustomRenderOptions) => {
|
||||||
|
const mockStore = createMockStore(options?.mockStore);
|
||||||
|
|
||||||
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<BookerStoreContext.Provider value={mockStore}>{children}</BookerStoreContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
return render(ui, { wrapper: Wrapper, ...options });
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from "@testing-library/react";
|
||||||
|
|
||||||
|
export { renderWithBookerStore as render };
|
||||||
@@ -2,6 +2,7 @@ import { useCallback, useMemo, useRef } from "react";
|
|||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings";
|
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
|
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
|
||||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||||
import type { Slot } from "@calcom/features/schedules/lib/use-schedule/types";
|
import type { Slot } from "@calcom/features/schedules/lib/use-schedule/types";
|
||||||
@@ -13,7 +14,6 @@ import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
|||||||
import classNames from "@calcom/ui/classNames";
|
import classNames from "@calcom/ui/classNames";
|
||||||
|
|
||||||
import { AvailableTimesHeader } from "../../components/AvailableTimesHeader";
|
import { AvailableTimesHeader } from "../../components/AvailableTimesHeader";
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
import type { useScheduleForEventReturnType } from "../utils/event";
|
import type { useScheduleForEventReturnType } from "../utils/event";
|
||||||
import { getQueryParam } from "../utils/query-param";
|
import { getQueryParam } from "../utils/query-param";
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ type AvailableTimeSlotsProps = {
|
|||||||
*/
|
*/
|
||||||
unavailableTimeSlots: string[];
|
unavailableTimeSlots: string[];
|
||||||
confirmButtonDisabled?: boolean;
|
confirmButtonDisabled?: boolean;
|
||||||
|
onAvailableTimeSlotSelect: (time: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,17 +73,17 @@ export const AvailableTimeSlots = ({
|
|||||||
unavailableTimeSlots,
|
unavailableTimeSlots,
|
||||||
confirmButtonDisabled,
|
confirmButtonDisabled,
|
||||||
confirmStepClassNames,
|
confirmStepClassNames,
|
||||||
|
onAvailableTimeSlotSelect,
|
||||||
...props
|
...props
|
||||||
}: AvailableTimeSlotsProps) => {
|
}: AvailableTimeSlotsProps) => {
|
||||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
|
||||||
|
|
||||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
const setSeatedEventData = useBookerStoreContext((state) => state.setSeatedEventData);
|
||||||
const setSeatedEventData = useBookerStore((state) => state.setSeatedEventData);
|
|
||||||
const date = selectedDate || dayjs().format("YYYY-MM-DD");
|
const date = selectedDate || dayjs().format("YYYY-MM-DD");
|
||||||
const [layout] = useBookerStore((state) => [state.layout]);
|
const [layout] = useBookerStoreContext((state) => [state.layout]);
|
||||||
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
|
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const { setTentativeSelectedTimeslots, tentativeSelectedTimeslots } = useBookerStore((state) => ({
|
const { setTentativeSelectedTimeslots, tentativeSelectedTimeslots } = useBookerStoreContext((state) => ({
|
||||||
setTentativeSelectedTimeslots: state.setTentativeSelectedTimeslots,
|
setTentativeSelectedTimeslots: state.setTentativeSelectedTimeslots,
|
||||||
tentativeSelectedTimeslots: state.tentativeSelectedTimeslots,
|
tentativeSelectedTimeslots: state.tentativeSelectedTimeslots,
|
||||||
}));
|
}));
|
||||||
@@ -146,7 +147,9 @@ export const AvailableTimeSlots = ({
|
|||||||
showAvailableSeatsCount,
|
showAvailableSeatsCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setSelectedTimeslot(time);
|
|
||||||
|
onAvailableTimeSlotSelect(time);
|
||||||
|
|
||||||
const isTimeSlotAvailable = !unavailableTimeSlots.includes(time);
|
const isTimeSlotAvailable = !unavailableTimeSlots.includes(time);
|
||||||
if (skipConfirmStep && isTimeSlotAvailable) {
|
if (skipConfirmStep && isTimeSlotAvailable) {
|
||||||
onSubmit(time);
|
onSubmit(time);
|
||||||
@@ -156,12 +159,12 @@ export const AvailableTimeSlots = ({
|
|||||||
[
|
[
|
||||||
onSubmit,
|
onSubmit,
|
||||||
setSeatedEventData,
|
setSeatedEventData,
|
||||||
setSelectedTimeslot,
|
|
||||||
skipConfirmStep,
|
skipConfirmStep,
|
||||||
showAvailableSeatsCount,
|
showAvailableSeatsCount,
|
||||||
unavailableTimeSlots,
|
unavailableTimeSlots,
|
||||||
schedule,
|
schedule,
|
||||||
setTentativeSelectedTimeslots,
|
setTentativeSelectedTimeslots,
|
||||||
|
onAvailableTimeSlotSelect,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
|
|||||||
import type { FieldError } from "react-hook-form";
|
import type { FieldError } from "react-hook-form";
|
||||||
|
|
||||||
import { useIsPlatformBookerEmbed } from "@calcom/atoms/hooks/useIsPlatformBookerEmbed";
|
import { useIsPlatformBookerEmbed } from "@calcom/atoms/hooks/useIsPlatformBookerEmbed";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||||
import ServerTrans from "@calcom/lib/components/ServerTrans";
|
import ServerTrans from "@calcom/lib/components/ServerTrans";
|
||||||
import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants";
|
import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants";
|
||||||
@@ -16,7 +17,6 @@ import { Button } from "@calcom/ui/components/button";
|
|||||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||||
import { Form } from "@calcom/ui/components/form";
|
import { Form } from "@calcom/ui/components/form";
|
||||||
|
|
||||||
import { useBookerStore } from "../../store";
|
|
||||||
import { formatEventFromTime } from "../../utils/dates";
|
import { formatEventFromTime } from "../../utils/dates";
|
||||||
import { useBookerTime } from "../hooks/useBookerTime";
|
import { useBookerTime } from "../hooks/useBookerTime";
|
||||||
import type { UseBookingFormReturnType } from "../hooks/useBookingForm";
|
import type { UseBookingFormReturnType } from "../hooks/useBookingForm";
|
||||||
@@ -43,6 +43,7 @@ type BookEventFormProps = {
|
|||||||
confirmButton?: string;
|
confirmButton?: string;
|
||||||
backButton?: string;
|
backButton?: string;
|
||||||
};
|
};
|
||||||
|
timeslot: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BookEventForm = ({
|
export const BookEventForm = ({
|
||||||
@@ -62,6 +63,7 @@ export const BookEventForm = ({
|
|||||||
shouldRenderCaptcha,
|
shouldRenderCaptcha,
|
||||||
confirmButtonDisabled,
|
confirmButtonDisabled,
|
||||||
classNames,
|
classNames,
|
||||||
|
timeslot,
|
||||||
}: Omit<BookEventFormProps, "event"> & {
|
}: Omit<BookEventFormProps, "event"> & {
|
||||||
eventQuery: {
|
eventQuery: {
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
@@ -70,12 +72,11 @@ export const BookEventForm = ({
|
|||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const eventType = eventQuery.data;
|
const eventType = eventQuery.data;
|
||||||
const setFormValues = useBookerStore((state) => state.setFormValues);
|
const setFormValues = useBookerStoreContext((state) => state.setFormValues);
|
||||||
const bookingData = useBookerStore((state) => state.bookingData);
|
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||||
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
|
||||||
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
const username = useBookerStoreContext((state) => state.username);
|
||||||
const username = useBookerStore((state) => state.username);
|
const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
|
||||||
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
|
|
||||||
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
|
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
|
||||||
const { timeFormat, timezone } = useBookerTime();
|
const { timeFormat, timezone } = useBookerTime();
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import React from "react";
|
|||||||
|
|
||||||
import { useEventTypeById } from "@calcom/atoms/hooks/event-types/private/useEventTypeById";
|
import { useEventTypeById } from "@calcom/atoms/hooks/event-types/private/useEventTypeById";
|
||||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Badge } from "@calcom/ui/components/badge";
|
import { Badge } from "@calcom/ui/components/badge";
|
||||||
import { DialogContent } from "@calcom/ui/components/dialog";
|
import { DialogContent } from "@calcom/ui/components/dialog";
|
||||||
|
|
||||||
import { getDurationFormatted } from "../../../components/event-meta/Duration";
|
import { getDurationFormatted } from "../../../components/event-meta/Duration";
|
||||||
import { useBookerStore } from "../../store";
|
|
||||||
import { FromTime } from "../../utils/dates";
|
import { FromTime } from "../../utils/dates";
|
||||||
import { useEvent } from "../../utils/event";
|
import { useEvent } from "../../utils/event";
|
||||||
import { useBookerTime } from "../hooks/useBookerTime";
|
import { useBookerTime } from "../hooks/useBookerTime";
|
||||||
@@ -27,7 +27,7 @@ const PlatformBookEventFormWrapper = ({
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const eventId = useBookerStore((state) => state.eventId);
|
const eventId = useBookerStoreContext((state) => state.eventId);
|
||||||
const { data } = useEventTypeById(eventId);
|
const { data } = useEventTypeById(eventId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -44,8 +44,8 @@ export const BookEventFormWrapperComponent = ({
|
|||||||
eventLength?: number;
|
eventLength?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const { i18n, t } = useLocale();
|
const { i18n, t } = useLocale();
|
||||||
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
|
const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
|
||||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||||
const { timeFormat, timezone } = useBookerTime();
|
const { timeFormat, timezone } = useBookerTime();
|
||||||
if (!selectedTimeslot) {
|
if (!selectedTimeslot) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { render } from "@testing-library/react";
|
import React from "react";
|
||||||
import { vi, afterEach } from "vitest";
|
import { vi, afterEach } from "vitest";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
||||||
|
|
||||||
|
import { render } from "../__tests__/test-utils";
|
||||||
import { DatePicker } from "./DatePicker";
|
import { DatePicker } from "./DatePicker";
|
||||||
|
|
||||||
vi.mock("@calcom/features/calendars/DatePicker", () => {
|
vi.mock("@calcom/features/calendars/DatePicker", () => {
|
||||||
@@ -18,7 +19,7 @@ const noop = () => {
|
|||||||
|
|
||||||
describe("Tests for DatePicker Component", () => {
|
describe("Tests for DatePicker Component", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
DatePickerComponent.mockClear();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("It passes the loading prop to the DatePicker component", async () => {
|
test("It passes the loading prop to the DatePicker component", async () => {
|
||||||
@@ -60,7 +61,7 @@ describe("Tests for DatePicker Component", () => {
|
|||||||
|
|
||||||
test("when there are only slots in the next month, skip the current month", async () => {
|
test("when there are only slots in the next month, skip the current month", async () => {
|
||||||
// there'll be one slot open on this day, next month.
|
// there'll be one slot open on this day, next month.
|
||||||
const slotDate = dayjs().add("1", "month");
|
const slotDate = dayjs().add(1, "month");
|
||||||
render(
|
render(
|
||||||
<DatePicker
|
<DatePicker
|
||||||
event={{}}
|
event={{}}
|
||||||
@@ -72,7 +73,12 @@ describe("Tests for DatePicker Component", () => {
|
|||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
/>
|
/>,
|
||||||
|
{
|
||||||
|
mockStore: {
|
||||||
|
month: slotDate.format("YYYY-MM"), // Start with next month to simulate the auto-advance
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
// slot date is next month, so it'll have skipped the current month
|
// slot date is next month, so it'll have skipped the current month
|
||||||
// by setting the browsingDate to the next month.
|
// by setting the browsingDate to the next month.
|
||||||
@@ -94,13 +100,18 @@ describe("Tests for DatePicker Component", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
/>
|
/>,
|
||||||
|
{
|
||||||
|
mockStore: {
|
||||||
|
month: dayjs().add(1, "month").format("YYYY-MM"), // Start with next month to simulate the auto-advance
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
// slot date is next month, so it'll have skipped the current month
|
// slot date is next month, so it'll have skipped the current month
|
||||||
// by setting the browsingDate to the next month.
|
// by setting the browsingDate to the next month.
|
||||||
expect(DatePickerComponent).toHaveBeenCalledWith(
|
expect(DatePickerComponent).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
browsingDate: dayjs().add("1", "month").startOf("month"),
|
browsingDate: dayjs().add(1, "month").startOf("month"),
|
||||||
}),
|
}),
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { shallow } from "zustand/shallow";
|
|||||||
|
|
||||||
import type { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { DatePickerClassNames } from "@calcom/features/bookings/Booker/types";
|
import type { DatePickerClassNames } from "@calcom/features/bookings/Booker/types";
|
||||||
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
||||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
|
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
|
||||||
@@ -11,7 +12,6 @@ import type { User } from "@calcom/prisma/client";
|
|||||||
import type { PeriodData } from "@calcom/types/Event";
|
import type { PeriodData } from "@calcom/types/Event";
|
||||||
|
|
||||||
import type { Slots } from "../../types";
|
import type { Slots } from "../../types";
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
|
|
||||||
const useMoveToNextMonthOnNoAvailability = ({
|
const useMoveToNextMonthOnNoAvailability = ({
|
||||||
browsingDate,
|
browsingDate,
|
||||||
@@ -76,12 +76,12 @@ export const DatePicker = ({
|
|||||||
showNoAvailabilityDialog?: boolean;
|
showNoAvailabilityDialog?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { i18n } = useLocale();
|
const { i18n } = useLocale();
|
||||||
const [month, selectedDate, layout] = useBookerStore(
|
const [month, selectedDate, layout] = useBookerStoreContext(
|
||||||
(state) => [state.month, state.selectedDate, state.layout],
|
(state) => [state.month, state.selectedDate, state.layout],
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
|
|
||||||
const [setSelectedDate, setMonth, setDayCount] = useBookerStore(
|
const [setSelectedDate, setMonth, setDayCount] = useBookerStoreContext(
|
||||||
(state) => [state.setSelectedDate, state.setMonth, state.setDayCount],
|
(state) => [state.setSelectedDate, state.setMonth, state.setDayCount],
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { shallow } from "zustand/shallow";
|
|||||||
import { Timezone as PlatformTimezoneSelect } from "@calcom/atoms/timezone";
|
import { Timezone as PlatformTimezoneSelect } from "@calcom/atoms/timezone";
|
||||||
import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings";
|
import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { Timezone } from "@calcom/features/bookings/Booker/types";
|
import type { Timezone } from "@calcom/features/bookings/Booker/types";
|
||||||
import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText";
|
import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText";
|
||||||
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
|
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
|
||||||
@@ -19,7 +20,6 @@ import { EventTypeAutoTranslatedField } from "@calcom/prisma/enums";
|
|||||||
|
|
||||||
import i18nConfigration from "../../../../../i18n.json";
|
import i18nConfigration from "../../../../../i18n.json";
|
||||||
import { fadeInUp } from "../config";
|
import { fadeInUp } from "../config";
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
import { FromToTime } from "../utils/dates";
|
import { FromToTime } from "../utils/dates";
|
||||||
import { useBookerTime } from "./hooks/useBookerTime";
|
import { useBookerTime } from "./hooks/useBookerTime";
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@ export const EventMeta = ({
|
|||||||
locale,
|
locale,
|
||||||
timeZones,
|
timeZones,
|
||||||
children,
|
children,
|
||||||
|
selectedTimeslot,
|
||||||
roundRobinHideOrgAndTeam,
|
roundRobinHideOrgAndTeam,
|
||||||
}: {
|
}: {
|
||||||
event?: Pick<
|
event?: Pick<
|
||||||
@@ -91,17 +92,17 @@ export const EventMeta = ({
|
|||||||
locale?: string | null;
|
locale?: string | null;
|
||||||
timeZones?: Timezone[];
|
timeZones?: Timezone[];
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
selectedTimeslot: string | null;
|
||||||
roundRobinHideOrgAndTeam?: boolean;
|
roundRobinHideOrgAndTeam?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { timeFormat, timezone } = useBookerTime();
|
const { timeFormat, timezone } = useBookerTime();
|
||||||
const [setTimezone] = useTimePreferences((state) => [state.setTimezone]);
|
const [setTimezone] = useTimePreferences((state) => [state.setTimezone]);
|
||||||
const [setBookerStoreTimezone] = useBookerStore((state) => [state.setTimezone], shallow);
|
const [setBookerStoreTimezone] = useBookerStoreContext((state) => [state.setTimezone], shallow);
|
||||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||||
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
|
const bookerState = useBookerStoreContext((state) => state.state);
|
||||||
const bookerState = useBookerStore((state) => state.state);
|
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||||
const bookingData = useBookerStore((state) => state.bookingData);
|
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
|
||||||
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
const [seatedEventData, setSeatedEventData] = useBookerStoreContext(
|
||||||
const [seatedEventData, setSeatedEventData] = useBookerStore(
|
|
||||||
(state) => [state.seatedEventData, state.setSeatedEventData],
|
(state) => [state.seatedEventData, state.setSeatedEventData],
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { shallow } from "zustand/shallow";
|
|||||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||||
@@ -14,7 +15,6 @@ import { Icon } from "@calcom/ui/components/icon";
|
|||||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||||
|
|
||||||
import { TimeFormatToggle } from "../../components/TimeFormatToggle";
|
import { TimeFormatToggle } from "../../components/TimeFormatToggle";
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
import type { BookerLayout } from "../types";
|
import type { BookerLayout } from "../types";
|
||||||
|
|
||||||
export function Header({
|
export function Header({
|
||||||
@@ -36,10 +36,10 @@ export function Header({
|
|||||||
}) {
|
}) {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed();
|
||||||
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
|
const [layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow);
|
||||||
const selectedDateString = useBookerStore((state) => state.selectedDate);
|
const selectedDateString = useBookerStoreContext((state) => state.selectedDate);
|
||||||
const setSelectedDate = useBookerStore((state) => state.setSelectedDate);
|
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
|
||||||
const addToSelectedDate = useBookerStore((state) => state.addToSelectedDate);
|
const addToSelectedDate = useBookerStoreContext((state) => state.addToSelectedDate);
|
||||||
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
|
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
|
||||||
const today = dayjs();
|
const today = dayjs();
|
||||||
const selectedDate = selectedDateString ? dayjs(selectedDateString) : today;
|
const selectedDate = selectedDateString ? dayjs(selectedDateString) : today;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useMemo, useEffect } from "react";
|
import { useMemo, useEffect } from "react";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||||
import { Calendar } from "@calcom/features/calendars/weeklyview";
|
import { Calendar } from "@calcom/features/calendars/weeklyview";
|
||||||
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
|
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
|
||||||
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
|
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
|
||||||
import { localStorage } from "@calcom/lib/webstorage";
|
import { localStorage } from "@calcom/lib/webstorage";
|
||||||
|
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
import type { useScheduleForEventReturnType } from "../utils/event";
|
import type { useScheduleForEventReturnType } from "../utils/event";
|
||||||
import { getQueryParam } from "../utils/query-param";
|
import { getQueryParam } from "../utils/query-param";
|
||||||
import { useOverlayCalendarStore } from "./OverlayCalendar/store";
|
import { useOverlayCalendarStore } from "./OverlayCalendar/store";
|
||||||
@@ -25,9 +25,9 @@ export const LargeCalendar = ({
|
|||||||
data?: Pick<BookerEvent, "length"> | null;
|
data?: Pick<BookerEvent, "length"> | null;
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
|
||||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
const setSelectedTimeslot = useBookerStoreContext((state) => state.setSelectedTimeslot);
|
||||||
const selectedEventDuration = useBookerStore((state) => state.selectedDuration);
|
const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||||
const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates);
|
const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates);
|
||||||
const displayOverlay =
|
const displayOverlay =
|
||||||
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");
|
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");
|
||||||
|
|||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Switch } from "@calcom/ui/components/form";
|
|
||||||
import { Button } from "@calcom/ui/components/button";
|
|
||||||
import classNames from "@calcom/ui/classNames";
|
import classNames from "@calcom/ui/classNames";
|
||||||
|
import { Button } from "@calcom/ui/components/button";
|
||||||
|
import { Switch } from "@calcom/ui/components/form";
|
||||||
|
|
||||||
import { useBookerStore } from "../../store";
|
|
||||||
import { useOverlayCalendarStore } from "./store";
|
import { useOverlayCalendarStore } from "./store";
|
||||||
|
|
||||||
interface OverlayCalendarSwitchProps {
|
interface OverlayCalendarSwitchProps {
|
||||||
@@ -20,7 +20,7 @@ export function OverlayCalendarSwitch({ enabled, hasSession, onStateChange }: Ov
|
|||||||
const setCalendarSettingsOverlay = useOverlayCalendarStore(
|
const setCalendarSettingsOverlay = useOverlayCalendarStore(
|
||||||
(state) => state.setCalendarSettingsOverlayModal
|
(state) => state.setCalendarSettingsOverlayModal
|
||||||
);
|
);
|
||||||
const layout = useBookerStore((state) => state.layout);
|
const layout = useBookerStoreContext((state) => state.layout);
|
||||||
const switchEnabled = enabled;
|
const switchEnabled = enabled;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type { MotionProps } from "framer-motion";
|
|||||||
import { m } from "framer-motion";
|
import { m } from "framer-motion";
|
||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import classNames from "@calcom/ui/classNames";
|
import classNames from "@calcom/ui/classNames";
|
||||||
|
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
import type { BookerAreas, BookerLayout } from "../types";
|
import type { BookerAreas, BookerLayout } from "../types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +44,7 @@ export const BookerSection = forwardRef<HTMLDivElement, BookerSectionProps>(func
|
|||||||
{ children, area, visible, className, ...props },
|
{ children, area, visible, className, ...props },
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const layout = useBookerStore((state) => state.layout);
|
const layout = useBookerStoreContext((state) => state.layout);
|
||||||
let gridClassName: string;
|
let gridClassName: string;
|
||||||
|
|
||||||
if (typeof area === "string") {
|
if (typeof area === "string") {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { useEffect, useRef } from "react";
|
|||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
|
|
||||||
import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||||
import type { BookerLayouts } from "@calcom/prisma/zod-utils";
|
import type { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||||
import { defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
|
import { defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { extraDaysConfig } from "../../config";
|
import { extraDaysConfig } from "../../config";
|
||||||
import { useBookerStore } from "../../store";
|
|
||||||
import type { BookerLayout } from "../../types";
|
import type { BookerLayout } from "../../types";
|
||||||
import { validateLayout } from "../../utils/layout";
|
import { validateLayout } from "../../utils/layout";
|
||||||
import { getQueryParam } from "../../utils/query-param";
|
import { getQueryParam } from "../../utils/query-param";
|
||||||
@@ -18,7 +18,7 @@ export type UseBookerLayoutType = ReturnType<typeof useBookerLayout>;
|
|||||||
export const useBookerLayout = (
|
export const useBookerLayout = (
|
||||||
profileBookerLayouts: BookerEvent["profile"]["bookerLayouts"] | undefined | null
|
profileBookerLayouts: BookerEvent["profile"]["bookerLayouts"] | undefined | null
|
||||||
) => {
|
) => {
|
||||||
const [_layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
|
const [_layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow);
|
||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed();
|
||||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||||
const isTablet = useMediaQuery("(max-width: 1024px)");
|
const isTablet = useMediaQuery("(max-width: 1024px)");
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
|
|
||||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
|
|
||||||
import { useTimePreferences } from "../../../lib/timePreferences";
|
import { useTimePreferences } from "../../../lib/timePreferences";
|
||||||
import { getBookerTimezone } from "../../utils/getBookerTimezone";
|
import { getBookerTimezone } from "../../utils/getBookerTimezone";
|
||||||
|
|
||||||
export const useBookerTime = () => {
|
export const useBookerTime = () => {
|
||||||
const [timezoneFromBookerStore] = useBookerStore((state) => [state.timezone], shallow);
|
const [timezoneFromBookerStore] = useBookerStoreContext((state) => [state.timezone], shallow);
|
||||||
const { timezone: timezoneFromTimePreferences, timeFormat } = useTimePreferences();
|
const { timezone: timezoneFromTimePreferences, timeFormat } = useTimePreferences();
|
||||||
const timezone = getBookerTimezone({
|
const timezone = getBookerTimezone({
|
||||||
storeTimezone: timezoneFromBookerStore,
|
storeTimezone: timezoneFromBookerStore,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
|||||||
import { useHandleBookEvent } from "@calcom/atoms/hooks/bookings/useHandleBookEvent";
|
import { useHandleBookEvent } from "@calcom/atoms/hooks/bookings/useHandleBookEvent";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
|
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||||
import { updateQueryParam, getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
import { updateQueryParam, getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
||||||
import { createBooking, createRecurringBooking, createInstantBooking } from "@calcom/features/bookings/lib";
|
import { createBooking, createRecurringBooking, createInstantBooking } from "@calcom/features/bookings/lib";
|
||||||
@@ -56,6 +57,28 @@ export interface IUseBookings {
|
|||||||
isBookingDryRun?: boolean;
|
isBookingDryRun?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getBaseBookingEventPayload = (booking: {
|
||||||
|
title?: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
eventTypeId?: number | null;
|
||||||
|
status?: BookingStatus;
|
||||||
|
paymentRequired: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
videoCallUrl?: string;
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
title: booking.title,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
eventTypeId: booking.eventTypeId,
|
||||||
|
status: booking.status,
|
||||||
|
paymentRequired: booking.paymentRequired,
|
||||||
|
isRecurring: booking.isRecurring,
|
||||||
|
videoCallUrl: booking.videoCallUrl,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getBookingSuccessfulEventPayload = (booking: {
|
const getBookingSuccessfulEventPayload = (booking: {
|
||||||
title?: string;
|
title?: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
@@ -69,18 +92,15 @@ const getBookingSuccessfulEventPayload = (booking: {
|
|||||||
}) => {
|
}) => {
|
||||||
return {
|
return {
|
||||||
uid: booking.uid,
|
uid: booking.uid,
|
||||||
title: booking.title,
|
...getBaseBookingEventPayload(booking),
|
||||||
startTime: booking.startTime,
|
|
||||||
endTime: booking.endTime,
|
|
||||||
eventTypeId: booking.eventTypeId,
|
|
||||||
status: booking.status,
|
|
||||||
paymentRequired: booking.paymentRequired,
|
|
||||||
isRecurring: booking.isRecurring,
|
|
||||||
videoCallUrl: booking.videoCallUrl,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRescheduleBookingSuccessfulEventPayload = getBookingSuccessfulEventPayload;
|
const getRescheduleBookingSuccessfulEventPayload = getBookingSuccessfulEventPayload;
|
||||||
|
|
||||||
|
export const getDryRunBookingSuccessfulEventPayload = getBaseBookingEventPayload;
|
||||||
|
|
||||||
|
export const getDryRunRescheduleBookingSuccessfulEventPayload = getDryRunBookingSuccessfulEventPayload;
|
||||||
export interface IUseBookingLoadingStates {
|
export interface IUseBookingLoadingStates {
|
||||||
creatingBooking: boolean;
|
creatingBooking: boolean;
|
||||||
creatingRecurringBooking: boolean;
|
creatingRecurringBooking: boolean;
|
||||||
@@ -117,21 +137,21 @@ export const useBookings = ({
|
|||||||
isBookingDryRun,
|
isBookingDryRun,
|
||||||
}: IUseBookings) => {
|
}: IUseBookings) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const eventSlug = useBookerStore((state) => state.eventSlug);
|
const eventSlug = useBookerStoreContext((state) => state.eventSlug);
|
||||||
const eventTypeId = useBookerStore((state) => state.eventId);
|
const eventTypeId = useBookerStoreContext((state) => state.eventId);
|
||||||
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
|
const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
|
||||||
|
|
||||||
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
|
||||||
const rescheduledBy = useBookerStore((state) => state.rescheduledBy);
|
const rescheduledBy = useBookerStoreContext((state) => state.rescheduledBy);
|
||||||
const bookingData = useBookerStore((state) => state.bookingData);
|
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||||
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
const timeslot = useBookerStoreContext((state) => state.selectedTimeslot);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const bookingSuccessRedirect = useBookingSuccessRedirect();
|
const bookingSuccessRedirect = useBookingSuccessRedirect();
|
||||||
const bookerFormErrorRef = useRef<HTMLDivElement>(null);
|
const bookerFormErrorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [instantMeetingTokenExpiryTime, setExpiryTime] = useState<Date | undefined>();
|
const [instantMeetingTokenExpiryTime, setExpiryTime] = useState<Date | undefined>();
|
||||||
const [instantVideoMeetingUrl, setInstantVideoMeetingUrl] = useState<string | undefined>();
|
const [instantVideoMeetingUrl, setInstantVideoMeetingUrl] = useState<string | undefined>();
|
||||||
const duration = useBookerStore((state) => state.selectedDuration);
|
const duration = useBookerStoreContext((state) => state.selectedDuration);
|
||||||
|
|
||||||
const isRescheduling = !!rescheduleUid && !!bookingData;
|
const isRescheduling = !!rescheduleUid && !!bookingData;
|
||||||
|
|
||||||
@@ -195,6 +215,30 @@ export const useBookings = ({
|
|||||||
mutationFn: createBooking,
|
mutationFn: createBooking,
|
||||||
onSuccess: (booking) => {
|
onSuccess: (booking) => {
|
||||||
if (booking.isDryRun) {
|
if (booking.isDryRun) {
|
||||||
|
const validDuration = event.data?.isDynamic
|
||||||
|
? duration || event.data?.length
|
||||||
|
: duration && event.data?.metadata?.multipleDuration?.includes(duration)
|
||||||
|
? duration
|
||||||
|
: event.data?.length;
|
||||||
|
|
||||||
|
if (isRescheduling) {
|
||||||
|
sdkActionManager?.fire(
|
||||||
|
"dryRunRescheduleBookingSuccessfulV2",
|
||||||
|
getDryRunRescheduleBookingSuccessfulEventPayload({
|
||||||
|
...booking,
|
||||||
|
isRecurring: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sdkActionManager?.fire(
|
||||||
|
"dryRunBookingSuccessfulV2",
|
||||||
|
getDryRunBookingSuccessfulEventPayload({
|
||||||
|
...booking,
|
||||||
|
isRecurring: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
router.push("/booking/dry-run-successful");
|
router.push("/booking/dry-run-successful");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -342,6 +386,30 @@ export const useBookings = ({
|
|||||||
const booking = bookings[0] || {};
|
const booking = bookings[0] || {};
|
||||||
|
|
||||||
if (booking.isDryRun) {
|
if (booking.isDryRun) {
|
||||||
|
if (isRescheduling) {
|
||||||
|
sdkActionManager?.fire("dryRunRescheduleBookingSuccessfulV2", {
|
||||||
|
...getDryRunRescheduleBookingSuccessfulEventPayload({
|
||||||
|
...booking,
|
||||||
|
isRecurring: true,
|
||||||
|
}),
|
||||||
|
allBookings: bookings.map((booking) => ({
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sdkActionManager?.fire("dryRunBookingSuccessfulV2", {
|
||||||
|
...getDryRunBookingSuccessfulEventPayload({
|
||||||
|
...booking,
|
||||||
|
isRecurring: true,
|
||||||
|
}),
|
||||||
|
allBookings: bookings.map((booking) => ({
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
router.push("/booking/dry-run-successful");
|
router.push("/booking/dry-run-successful");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useRef } from "react";
|
|||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { useSlotReservationId } from "@calcom/features/bookings/Booker/useSlotReservationId";
|
import { useSlotReservationId } from "@calcom/features/bookings/Booker/useSlotReservationId";
|
||||||
import { isBookingDryRun } from "@calcom/features/bookings/Booker/utils/isBookingDryRun";
|
import { isBookingDryRun } from "@calcom/features/bookings/Booker/utils/isBookingDryRun";
|
||||||
import {
|
import {
|
||||||
@@ -78,10 +78,10 @@ const useQuickAvailabilityChecks = ({
|
|||||||
export type UseSlotsReturnType = ReturnType<typeof useSlots>;
|
export type UseSlotsReturnType = ReturnType<typeof useSlots>;
|
||||||
|
|
||||||
export const useSlots = (event: { id: number; length: number } | null) => {
|
export const useSlots = (event: { id: number; length: number } | null) => {
|
||||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||||
const searchParams = useCompatSearchParams();
|
const searchParams = useCompatSearchParams();
|
||||||
const [selectedTimeslot, setSelectedTimeslot, tentativeSelectedTimeslots, setTentativeSelectedTimeslots] =
|
const [selectedTimeslot, setSelectedTimeslot, tentativeSelectedTimeslots, setTentativeSelectedTimeslots] =
|
||||||
useBookerStore(
|
useBookerStoreContext(
|
||||||
(state) => [
|
(state) => [
|
||||||
state.selectedTimeslot,
|
state.selectedTimeslot,
|
||||||
state.setSelectedTimeslot,
|
state.setSelectedTimeslot,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { updateQueryParam, getQueryParam, removeQueryParam } from "./utils/query
|
|||||||
* Arguments passed into store initializer, containing
|
* Arguments passed into store initializer, containing
|
||||||
* the event data.
|
* the event data.
|
||||||
*/
|
*/
|
||||||
type StoreInitializeType = {
|
export type StoreInitializeType = {
|
||||||
username: string;
|
username: string;
|
||||||
eventSlug: string;
|
eventSlug: string;
|
||||||
// Month can be undefined if it's not passed in as a prop.
|
// Month can be undefined if it's not passed in as a prop.
|
||||||
@@ -174,279 +174,281 @@ export type BookerStore = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The booker store contains the data of the component's
|
* Creates a new booker store instance
|
||||||
* current state. This data can be reused within child components
|
|
||||||
* by importing this hook.
|
|
||||||
*
|
|
||||||
* See comments in interface above for more information on it's specific values.
|
|
||||||
*/
|
*/
|
||||||
export const useBookerStore = createWithEqualityFn<BookerStore>((set, get) => ({
|
export const createBookerStore = () =>
|
||||||
state: "loading",
|
createWithEqualityFn<BookerStore>((set, get) => ({
|
||||||
setState: (state: BookerState) => set({ state }),
|
state: "loading",
|
||||||
layout: BookerLayouts.MONTH_VIEW,
|
setState: (state: BookerState) => set({ state }),
|
||||||
setLayout: (layout: BookerLayout) => {
|
layout: BookerLayouts.MONTH_VIEW,
|
||||||
// If we switch to a large layout and don't have a date selected yet,
|
setLayout: (layout: BookerLayout) => {
|
||||||
// we selected it here, so week title is rendered properly.
|
// If we switch to a large layout and don't have a date selected yet,
|
||||||
if (["week_view", "column_view"].includes(layout) && !get().selectedDate) {
|
// we selected it here, so week title is rendered properly.
|
||||||
set({ selectedDate: dayjs().format("YYYY-MM-DD") });
|
if (["week_view", "column_view"].includes(layout) && !get().selectedDate) {
|
||||||
}
|
set({ selectedDate: dayjs().format("YYYY-MM-DD") });
|
||||||
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
|
||||||
updateQueryParam("layout", layout);
|
|
||||||
}
|
|
||||||
return set({ layout });
|
|
||||||
},
|
|
||||||
selectedDate: getQueryParam("date") || null,
|
|
||||||
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
|
|
||||||
// unset selected date
|
|
||||||
if (!selectedDate) {
|
|
||||||
removeQueryParam("date");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentSelection = dayjs(get().selectedDate);
|
|
||||||
const newSelection = dayjs(selectedDate);
|
|
||||||
set({ selectedDate });
|
|
||||||
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
|
|
||||||
updateQueryParam("date", selectedDate ?? "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setting month make sure small calendar in fullscreen layouts also updates.
|
|
||||||
// preventMonthSwitching is true in monthly view
|
|
||||||
if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) {
|
|
||||||
set({ month: newSelection.format("YYYY-MM") });
|
|
||||||
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
|
|
||||||
updateQueryParam("month", newSelection.format("YYYY-MM"));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
selectedDatesAndTimes: null,
|
|
||||||
setSelectedDatesAndTimes: (selectedDatesAndTimes) => {
|
|
||||||
set({ selectedDatesAndTimes });
|
|
||||||
},
|
|
||||||
addToSelectedDate: (days: number) => {
|
|
||||||
const currentSelection = dayjs(get().selectedDate);
|
|
||||||
let newSelection = currentSelection.add(days, "day");
|
|
||||||
|
|
||||||
// If newSelection is before the current date, set it to today
|
|
||||||
if (newSelection.isBefore(dayjs(), "day")) {
|
|
||||||
newSelection = dayjs();
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSelectionFormatted = newSelection.format("YYYY-MM-DD");
|
|
||||||
|
|
||||||
if (newSelection.month() !== currentSelection.month()) {
|
|
||||||
set({ month: newSelection.format("YYYY-MM") });
|
|
||||||
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
||||||
updateQueryParam("month", newSelection.format("YYYY-MM"));
|
updateQueryParam("layout", layout);
|
||||||
|
}
|
||||||
|
return set({ layout });
|
||||||
|
},
|
||||||
|
selectedDate: getQueryParam("date") || null,
|
||||||
|
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
|
||||||
|
// unset selected date
|
||||||
|
if (!selectedDate) {
|
||||||
|
removeQueryParam("date");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
set({ selectedDate: newSelectionFormatted });
|
const currentSelection = dayjs(get().selectedDate);
|
||||||
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
const newSelection = dayjs(selectedDate);
|
||||||
updateQueryParam("date", newSelectionFormatted);
|
set({ selectedDate });
|
||||||
}
|
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
|
||||||
},
|
updateQueryParam("date", selectedDate ?? "");
|
||||||
username: null,
|
}
|
||||||
eventSlug: null,
|
|
||||||
eventId: null,
|
|
||||||
rescheduledBy: null,
|
|
||||||
verifiedEmail: null,
|
|
||||||
setVerifiedEmail: (email: string | null) => {
|
|
||||||
set({ verifiedEmail: email });
|
|
||||||
},
|
|
||||||
month:
|
|
||||||
getQueryParam("month") ||
|
|
||||||
(getQueryParam("date") && dayjs(getQueryParam("date")).isValid()
|
|
||||||
? dayjs(getQueryParam("date")).format("YYYY-MM")
|
|
||||||
: null) ||
|
|
||||||
dayjs().format("YYYY-MM"),
|
|
||||||
setMonth: (month: string | null) => {
|
|
||||||
if (!month) {
|
|
||||||
removeQueryParam("month");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
set({ month, selectedTimeslot: null });
|
|
||||||
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
|
||||||
updateQueryParam("month", month ?? "");
|
|
||||||
}
|
|
||||||
get().setSelectedDate({ date: null });
|
|
||||||
},
|
|
||||||
dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null,
|
|
||||||
setDayCount: (dayCount: number | null) => {
|
|
||||||
set({ dayCount });
|
|
||||||
},
|
|
||||||
isTeamEvent: false,
|
|
||||||
seatedEventData: {
|
|
||||||
seatsPerTimeSlot: undefined,
|
|
||||||
attendees: undefined,
|
|
||||||
bookingUid: undefined,
|
|
||||||
showAvailableSeatsCount: true,
|
|
||||||
},
|
|
||||||
setSeatedEventData: (seatedEventData: SeatedEventData) => {
|
|
||||||
set({ seatedEventData });
|
|
||||||
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
|
||||||
updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// This is different from timeZone in timePreferencesStore, because timeZone in timePreferencesStore is the preferred timezone of the booker,
|
|
||||||
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
|
|
||||||
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
|
|
||||||
timezone: getQueryParam("cal.tz") ?? null,
|
|
||||||
setTimezone: (timezone: string | null) => {
|
|
||||||
set({ timezone });
|
|
||||||
},
|
|
||||||
initialize: ({
|
|
||||||
username,
|
|
||||||
eventSlug,
|
|
||||||
month,
|
|
||||||
eventId,
|
|
||||||
rescheduleUid = null,
|
|
||||||
rescheduledBy = null,
|
|
||||||
bookingUid = null,
|
|
||||||
bookingData = null,
|
|
||||||
layout,
|
|
||||||
isTeamEvent,
|
|
||||||
durationConfig,
|
|
||||||
org,
|
|
||||||
isInstantMeeting,
|
|
||||||
timezone = null,
|
|
||||||
teamMemberEmail,
|
|
||||||
crmOwnerRecordType,
|
|
||||||
crmAppSlug,
|
|
||||||
crmRecordId,
|
|
||||||
isPlatform = false,
|
|
||||||
allowUpdatingUrlParams = true,
|
|
||||||
}: StoreInitializeType) => {
|
|
||||||
const selectedDateInStore = get().selectedDate;
|
|
||||||
|
|
||||||
if (
|
// Setting month make sure small calendar in fullscreen layouts also updates.
|
||||||
get().username === username &&
|
// preventMonthSwitching is true in monthly view
|
||||||
get().eventSlug === eventSlug &&
|
if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) {
|
||||||
get().month === month &&
|
set({ month: newSelection.format("YYYY-MM") });
|
||||||
get().eventId === eventId &&
|
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
|
||||||
get().rescheduleUid === rescheduleUid &&
|
updateQueryParam("month", newSelection.format("YYYY-MM"));
|
||||||
get().bookingUid === bookingUid &&
|
}
|
||||||
get().bookingData?.responses.email === bookingData?.responses.email &&
|
}
|
||||||
get().layout === layout &&
|
},
|
||||||
get().timezone === timezone &&
|
selectedDatesAndTimes: null,
|
||||||
get().rescheduledBy === rescheduledBy &&
|
setSelectedDatesAndTimes: (selectedDatesAndTimes) => {
|
||||||
get().teamMemberEmail === teamMemberEmail &&
|
set({ selectedDatesAndTimes });
|
||||||
get().crmOwnerRecordType === crmOwnerRecordType &&
|
},
|
||||||
get().crmAppSlug === crmAppSlug &&
|
addToSelectedDate: (days: number) => {
|
||||||
get().crmRecordId === crmRecordId
|
const currentSelection = dayjs(get().selectedDate);
|
||||||
)
|
let newSelection = currentSelection.add(days, "day");
|
||||||
return;
|
|
||||||
set({
|
// If newSelection is before the current date, set it to today
|
||||||
|
if (newSelection.isBefore(dayjs(), "day")) {
|
||||||
|
newSelection = dayjs();
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSelectionFormatted = newSelection.format("YYYY-MM-DD");
|
||||||
|
|
||||||
|
if (newSelection.month() !== currentSelection.month()) {
|
||||||
|
set({ month: newSelection.format("YYYY-MM") });
|
||||||
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
||||||
|
updateQueryParam("month", newSelection.format("YYYY-MM"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ selectedDate: newSelectionFormatted });
|
||||||
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
||||||
|
updateQueryParam("date", newSelectionFormatted);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
username: null,
|
||||||
|
eventSlug: null,
|
||||||
|
eventId: null,
|
||||||
|
rescheduledBy: null,
|
||||||
|
verifiedEmail: null,
|
||||||
|
setVerifiedEmail: (email: string | null) => {
|
||||||
|
set({ verifiedEmail: email });
|
||||||
|
},
|
||||||
|
month:
|
||||||
|
getQueryParam("month") ||
|
||||||
|
(getQueryParam("date") && dayjs(getQueryParam("date")).isValid()
|
||||||
|
? dayjs(getQueryParam("date")).format("YYYY-MM")
|
||||||
|
: null) ||
|
||||||
|
dayjs().format("YYYY-MM"),
|
||||||
|
setMonth: (month: string | null) => {
|
||||||
|
if (!month) {
|
||||||
|
removeQueryParam("month");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set({ month, selectedTimeslot: null });
|
||||||
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
||||||
|
updateQueryParam("month", month ?? "");
|
||||||
|
}
|
||||||
|
get().setSelectedDate({ date: null });
|
||||||
|
},
|
||||||
|
dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null,
|
||||||
|
setDayCount: (dayCount: number | null) => {
|
||||||
|
set({ dayCount });
|
||||||
|
},
|
||||||
|
isTeamEvent: false,
|
||||||
|
seatedEventData: {
|
||||||
|
seatsPerTimeSlot: undefined,
|
||||||
|
attendees: undefined,
|
||||||
|
bookingUid: undefined,
|
||||||
|
showAvailableSeatsCount: true,
|
||||||
|
},
|
||||||
|
setSeatedEventData: (seatedEventData: SeatedEventData) => {
|
||||||
|
set({ seatedEventData });
|
||||||
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
||||||
|
updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// This is different from timeZone in timePreferencesStore, because timeZone in timePreferencesStore is the preferred timezone of the booker,
|
||||||
|
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
|
||||||
|
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
|
||||||
|
timezone: getQueryParam("cal.tz") ?? null,
|
||||||
|
setTimezone: (timezone: string | null) => {
|
||||||
|
set({ timezone });
|
||||||
|
},
|
||||||
|
initialize: ({
|
||||||
username,
|
username,
|
||||||
eventSlug,
|
eventSlug,
|
||||||
|
month,
|
||||||
eventId,
|
eventId,
|
||||||
org,
|
rescheduleUid = null,
|
||||||
rescheduleUid,
|
rescheduledBy = null,
|
||||||
rescheduledBy,
|
bookingUid = null,
|
||||||
bookingUid,
|
bookingData = null,
|
||||||
bookingData,
|
layout,
|
||||||
layout: layout || BookerLayouts.MONTH_VIEW,
|
isTeamEvent,
|
||||||
isTeamEvent: isTeamEvent || false,
|
|
||||||
durationConfig,
|
durationConfig,
|
||||||
timezone,
|
org,
|
||||||
// Preselect today's date in week / column view, since they use this to show the week title.
|
isInstantMeeting,
|
||||||
selectedDate:
|
timezone = null,
|
||||||
selectedDateInStore ||
|
|
||||||
(["week_view", "column_view"].includes(layout) ? dayjs().format("YYYY-MM-DD") : null),
|
|
||||||
teamMemberEmail,
|
teamMemberEmail,
|
||||||
crmOwnerRecordType,
|
crmOwnerRecordType,
|
||||||
crmAppSlug,
|
crmAppSlug,
|
||||||
crmRecordId,
|
crmRecordId,
|
||||||
isPlatform,
|
isPlatform = false,
|
||||||
allowUpdatingUrlParams,
|
allowUpdatingUrlParams = true,
|
||||||
});
|
}: StoreInitializeType) => {
|
||||||
|
const selectedDateInStore = get().selectedDate;
|
||||||
|
|
||||||
if (durationConfig?.includes(Number(getQueryParam("duration")))) {
|
if (
|
||||||
|
get().username === username &&
|
||||||
|
get().eventSlug === eventSlug &&
|
||||||
|
get().month === month &&
|
||||||
|
get().eventId === eventId &&
|
||||||
|
get().rescheduleUid === rescheduleUid &&
|
||||||
|
get().bookingUid === bookingUid &&
|
||||||
|
get().bookingData?.responses.email === bookingData?.responses.email &&
|
||||||
|
get().layout === layout &&
|
||||||
|
get().timezone === timezone &&
|
||||||
|
get().rescheduledBy === rescheduledBy &&
|
||||||
|
get().teamMemberEmail === teamMemberEmail &&
|
||||||
|
get().crmOwnerRecordType === crmOwnerRecordType &&
|
||||||
|
get().crmAppSlug === crmAppSlug &&
|
||||||
|
get().crmRecordId === crmRecordId
|
||||||
|
)
|
||||||
|
return;
|
||||||
set({
|
set({
|
||||||
selectedDuration: Number(getQueryParam("duration")),
|
username,
|
||||||
|
eventSlug,
|
||||||
|
eventId,
|
||||||
|
org,
|
||||||
|
rescheduleUid,
|
||||||
|
rescheduledBy,
|
||||||
|
bookingUid,
|
||||||
|
bookingData,
|
||||||
|
layout: layout || BookerLayouts.MONTH_VIEW,
|
||||||
|
isTeamEvent: isTeamEvent || false,
|
||||||
|
durationConfig,
|
||||||
|
timezone,
|
||||||
|
// Preselect today's date in week / column view, since they use this to show the week title.
|
||||||
|
selectedDate:
|
||||||
|
selectedDateInStore ||
|
||||||
|
(["week_view", "column_view"].includes(layout) ? dayjs().format("YYYY-MM-DD") : null),
|
||||||
|
teamMemberEmail,
|
||||||
|
crmOwnerRecordType,
|
||||||
|
crmAppSlug,
|
||||||
|
crmRecordId,
|
||||||
|
isPlatform,
|
||||||
|
allowUpdatingUrlParams,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
removeQueryParam("duration");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unset selected timeslot if user is rescheduling. This could happen
|
if (durationConfig?.includes(Number(getQueryParam("duration")))) {
|
||||||
// if the user reschedules a booking right after the confirmation page.
|
set({
|
||||||
// In that case the time would still be store in the store, this way we
|
selectedDuration: Number(getQueryParam("duration")),
|
||||||
// force clear this.
|
});
|
||||||
// Also, fetch the original booking duration if user is rescheduling and
|
} else {
|
||||||
// update the selectedDuration
|
removeQueryParam("duration");
|
||||||
if (rescheduleUid && bookingData) {
|
|
||||||
set({ selectedTimeslot: null });
|
|
||||||
const originalBookingLength = dayjs(bookingData?.endTime).diff(
|
|
||||||
dayjs(bookingData?.startTime),
|
|
||||||
"minutes"
|
|
||||||
);
|
|
||||||
set({ selectedDuration: originalBookingLength });
|
|
||||||
if (!isPlatform || allowUpdatingUrlParams) {
|
|
||||||
updateQueryParam("duration", originalBookingLength ?? "");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (month) set({ month });
|
|
||||||
|
|
||||||
if (isInstantMeeting) {
|
// Unset selected timeslot if user is rescheduling. This could happen
|
||||||
const month = dayjs().format("YYYY-MM");
|
// if the user reschedules a booking right after the confirmation page.
|
||||||
const selectedDate = dayjs().format("YYYY-MM-DD");
|
// In that case the time would still be store in the store, this way we
|
||||||
const selectedTimeslot = new Date().toISOString();
|
// force clear this.
|
||||||
set({
|
// Also, fetch the original booking duration if user is rescheduling and
|
||||||
month,
|
// update the selectedDuration
|
||||||
selectedDate,
|
if (rescheduleUid && bookingData) {
|
||||||
selectedTimeslot,
|
set({ selectedTimeslot: null });
|
||||||
isInstantMeeting,
|
const originalBookingLength = dayjs(bookingData?.endTime).diff(
|
||||||
});
|
dayjs(bookingData?.startTime),
|
||||||
|
"minutes"
|
||||||
|
);
|
||||||
|
set({ selectedDuration: originalBookingLength });
|
||||||
|
if (!isPlatform || allowUpdatingUrlParams) {
|
||||||
|
updateQueryParam("duration", originalBookingLength ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (month) set({ month });
|
||||||
|
|
||||||
if (!isPlatform || allowUpdatingUrlParams) {
|
if (isInstantMeeting) {
|
||||||
updateQueryParam("month", month);
|
const month = dayjs().format("YYYY-MM");
|
||||||
updateQueryParam("date", selectedDate ?? "");
|
const selectedDate = dayjs().format("YYYY-MM-DD");
|
||||||
|
const selectedTimeslot = new Date().toISOString();
|
||||||
|
set({
|
||||||
|
month,
|
||||||
|
selectedDate,
|
||||||
|
selectedTimeslot,
|
||||||
|
isInstantMeeting,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isPlatform || allowUpdatingUrlParams) {
|
||||||
|
updateQueryParam("month", month);
|
||||||
|
updateQueryParam("date", selectedDate ?? "");
|
||||||
|
updateQueryParam("slot", selectedTimeslot ?? "", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//removeQueryParam("layout");
|
||||||
|
},
|
||||||
|
durationConfig: null,
|
||||||
|
selectedDuration: null,
|
||||||
|
setSelectedDuration: (selectedDuration: number | null) => {
|
||||||
|
set({ selectedDuration });
|
||||||
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
||||||
|
updateQueryParam("duration", selectedDuration ?? "");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setBookingData: (bookingData: GetBookingType | null | undefined) => {
|
||||||
|
set({ bookingData: bookingData ?? null });
|
||||||
|
},
|
||||||
|
recurringEventCount: null,
|
||||||
|
setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }),
|
||||||
|
occurenceCount: null,
|
||||||
|
setOccurenceCount: (occurenceCount: number | null) => set({ occurenceCount }),
|
||||||
|
rescheduleUid: null,
|
||||||
|
bookingData: null,
|
||||||
|
bookingUid: null,
|
||||||
|
selectedTimeslot: getQueryParam("slot") || null,
|
||||||
|
tentativeSelectedTimeslots: [],
|
||||||
|
setTentativeSelectedTimeslots: (tentativeSelectedTimeslots: string[]) => {
|
||||||
|
set({ tentativeSelectedTimeslots });
|
||||||
|
},
|
||||||
|
setSelectedTimeslot: (selectedTimeslot: string | null) => {
|
||||||
|
set({ selectedTimeslot });
|
||||||
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
||||||
updateQueryParam("slot", selectedTimeslot ?? "", false);
|
updateQueryParam("slot", selectedTimeslot ?? "", false);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
//removeQueryParam("layout");
|
formValues: {},
|
||||||
},
|
setFormValues: (formValues: Record<string, any>) => {
|
||||||
durationConfig: null,
|
set({ formValues });
|
||||||
selectedDuration: null,
|
},
|
||||||
setSelectedDuration: (selectedDuration: number | null) => {
|
org: null,
|
||||||
set({ selectedDuration });
|
setOrg: (org: string | null | undefined) => {
|
||||||
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
set({ org });
|
||||||
updateQueryParam("duration", selectedDuration ?? "");
|
},
|
||||||
}
|
isPlatform: false,
|
||||||
},
|
allowUpdatingUrlParams: true,
|
||||||
setBookingData: (bookingData: GetBookingType | null | undefined) => {
|
}));
|
||||||
set({ bookingData: bookingData ?? null });
|
|
||||||
},
|
/**
|
||||||
recurringEventCount: null,
|
* Default global store instance for backward compatibility
|
||||||
setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }),
|
*/
|
||||||
occurenceCount: null,
|
export const useBookerStore = createBookerStore();
|
||||||
setOccurenceCount: (occurenceCount: number | null) => set({ occurenceCount }),
|
|
||||||
rescheduleUid: null,
|
|
||||||
bookingData: null,
|
|
||||||
bookingUid: null,
|
|
||||||
selectedTimeslot: getQueryParam("slot") || null,
|
|
||||||
tentativeSelectedTimeslots: [],
|
|
||||||
setTentativeSelectedTimeslots: (tentativeSelectedTimeslots: string[]) => {
|
|
||||||
set({ tentativeSelectedTimeslots });
|
|
||||||
},
|
|
||||||
setSelectedTimeslot: (selectedTimeslot: string | null) => {
|
|
||||||
set({ selectedTimeslot });
|
|
||||||
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
|
||||||
updateQueryParam("slot", selectedTimeslot ?? "", false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formValues: {},
|
|
||||||
setFormValues: (formValues: Record<string, any>) => {
|
|
||||||
set({ formValues });
|
|
||||||
},
|
|
||||||
org: null,
|
|
||||||
setOrg: (org: string | null | undefined) => {
|
|
||||||
set({ org });
|
|
||||||
},
|
|
||||||
isPlatform: false,
|
|
||||||
allowUpdatingUrlParams: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const useInitializeBookerStore = ({
|
export const useInitializeBookerStore = ({
|
||||||
username,
|
username,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
|
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { useSchedule } from "@calcom/features/schedules/lib/use-schedule/useSchedule";
|
import { useSchedule } from "@calcom/features/schedules/lib/use-schedule/useSchedule";
|
||||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
import { useBookerTime } from "../components/hooks/useBookerTime";
|
import { useBookerTime } from "../components/hooks/useBookerTime";
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
|
|
||||||
export type useEventReturnType = ReturnType<typeof useEvent>;
|
export type useEventReturnType = ReturnType<typeof useEvent>;
|
||||||
export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEvent>;
|
export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEvent>;
|
||||||
@@ -19,7 +19,7 @@ export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEven
|
|||||||
* of combining multiple conditional hooks.
|
* of combining multiple conditional hooks.
|
||||||
*/
|
*/
|
||||||
export const useEvent = (props?: { fromRedirectOfNonOrgLink?: boolean; disabled?: boolean }) => {
|
export const useEvent = (props?: { fromRedirectOfNonOrgLink?: boolean; disabled?: boolean }) => {
|
||||||
const [username, eventSlug, isTeamEvent, org] = useBookerStore(
|
const [username, eventSlug, isTeamEvent, org] = useBookerStoreContext(
|
||||||
(state) => [state.username, state.eventSlug, state.isTeamEvent, state.org],
|
(state) => [state.username, state.eventSlug, state.isTeamEvent, state.org],
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
@@ -89,7 +89,7 @@ export const useScheduleForEvent = ({
|
|||||||
useApiV2?: boolean;
|
useApiV2?: boolean;
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const { timezone } = useBookerTime();
|
const { timezone } = useBookerTime();
|
||||||
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStore(
|
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStoreContext(
|
||||||
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
|
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useMemo } from "react";
|
|||||||
|
|
||||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { OutOfOfficeInSlots } from "@calcom/features/bookings/Booker/components/OutOfOfficeInSlots";
|
import { OutOfOfficeInSlots } from "@calcom/features/bookings/Booker/components/OutOfOfficeInSlots";
|
||||||
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
|
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
|
||||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||||
@@ -19,7 +20,6 @@ import { Icon } from "@calcom/ui/components/icon";
|
|||||||
import { SkeletonText } from "@calcom/ui/components/skeleton";
|
import { SkeletonText } from "@calcom/ui/components/skeleton";
|
||||||
|
|
||||||
import { useBookerTime } from "../Booker/components/hooks/useBookerTime";
|
import { useBookerTime } from "../Booker/components/hooks/useBookerTime";
|
||||||
import { useBookerStore } from "../Booker/store";
|
|
||||||
import { getQueryParam } from "../Booker/utils/query-param";
|
import { getQueryParam } from "../Booker/utils/query-param";
|
||||||
import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay";
|
import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay";
|
||||||
import type { Slots } from "../types";
|
import type { Slots } from "../types";
|
||||||
@@ -112,8 +112,8 @@ const SlotItem = ({
|
|||||||
getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault");
|
getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault");
|
||||||
|
|
||||||
const { timeFormat, timezone } = useBookerTime();
|
const { timeFormat, timezone } = useBookerTime();
|
||||||
const bookingData = useBookerStore((state) => state.bookingData);
|
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||||
const layout = useBookerStore((state) => state.layout);
|
const layout = useBookerStoreContext((state) => state.layout);
|
||||||
const hasTimeSlots = !!seatsPerTimeSlot;
|
const hasTimeSlots = !!seatsPerTimeSlot;
|
||||||
const computedDateWithUsersTimezone = dayjs.utc(slot.time).tz(timezone);
|
const computedDateWithUsersTimezone = dayjs.utc(slot.time).tz(timezone);
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ const SlotItem = ({
|
|||||||
|
|
||||||
const offset = (usersTimezoneDate.utcOffset() - nowDate.utcOffset()) / 60;
|
const offset = (usersTimezoneDate.utcOffset() - nowDate.utcOffset()) / 60;
|
||||||
|
|
||||||
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
|
const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
|
||||||
|
|
||||||
const { isOverlapping, overlappingTimeEnd, overlappingTimeStart } = useCheckOverlapWithOverlay({
|
const { isOverlapping, overlappingTimeEnd, overlappingTimeStart } = useCheckOverlapWithOverlay({
|
||||||
start: computedDateWithUsersTimezone,
|
start: computedDateWithUsersTimezone,
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { shallow } from "zustand/shallow";
|
|||||||
|
|
||||||
import type { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { nameOfDay } from "@calcom/lib/weekday";
|
import { nameOfDay } from "@calcom/lib/weekday";
|
||||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||||
import classNames from "@calcom/ui/classNames";
|
import classNames from "@calcom/ui/classNames";
|
||||||
|
|
||||||
import { useBookerStore } from "../Booker/store";
|
|
||||||
import { TimeFormatToggle } from "./TimeFormatToggle";
|
import { TimeFormatToggle } from "./TimeFormatToggle";
|
||||||
|
|
||||||
type AvailableTimesHeaderProps = {
|
type AvailableTimesHeaderProps = {
|
||||||
@@ -28,7 +28,7 @@ export const AvailableTimesHeader = ({
|
|||||||
customClassNames,
|
customClassNames,
|
||||||
}: AvailableTimesHeaderProps) => {
|
}: AvailableTimesHeaderProps) => {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const [layout] = useBookerStore((state) => [state.layout], shallow);
|
const [layout] = useBookerStoreContext((state) => [state.layout], shallow);
|
||||||
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
|
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
|
||||||
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
|
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
|
||||||
const isToday = dayjs().isSame(date, "day");
|
const isToday = dayjs().isSame(date, "day");
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEffect, useRef } from "react";
|
|||||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
import { useShouldShowArrows } from "@calcom/features/apps/components/AllApps";
|
import { useShouldShowArrows } from "@calcom/features/apps/components/AllApps";
|
||||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import classNames from "@calcom/ui/classNames";
|
import classNames from "@calcom/ui/classNames";
|
||||||
@@ -45,7 +45,7 @@ export const EventDuration = ({
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
|
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const [selectedDuration, setSelectedDuration, state] = useBookerStore((state) => [
|
const [selectedDuration, setSelectedDuration, state] = useBookerStoreContext((state) => [
|
||||||
state.selectedDuration,
|
state.selectedDuration,
|
||||||
state.setSelectedDuration,
|
state.setSelectedDuration,
|
||||||
state.state,
|
state.state,
|
||||||
|
|||||||
@@ -550,6 +550,9 @@ async function handler(
|
|||||||
eventType.schedulingType === SchedulingType.ROUND_ROBIN
|
eventType.schedulingType === SchedulingType.ROUND_ROBIN
|
||||||
) {
|
) {
|
||||||
const bookingRepo = new BookingRepository(prisma);
|
const bookingRepo = new BookingRepository(prisma);
|
||||||
|
|
||||||
|
const requiresPayment = !Number.isNaN(paymentAppData.price) && paymentAppData.price > 0;
|
||||||
|
|
||||||
const existingBooking = await bookingRepo.getValidBookingFromEventTypeForAttendee({
|
const existingBooking = await bookingRepo.getValidBookingFromEventTypeForAttendee({
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
bookerEmail,
|
bookerEmail,
|
||||||
@@ -559,13 +562,20 @@ async function handler(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (existingBooking) {
|
if (existingBooking) {
|
||||||
|
const hasPayments = existingBooking.payment.length > 0;
|
||||||
|
const isPaidBooking = existingBooking.paid || !hasPayments;
|
||||||
|
|
||||||
|
const shouldShowPaymentForm = requiresPayment && !isPaidBooking;
|
||||||
|
|
||||||
|
const firstPayment = shouldShowPaymentForm ? existingBooking.payment[0] : undefined;
|
||||||
|
|
||||||
const bookingResponse = {
|
const bookingResponse = {
|
||||||
...existingBooking,
|
...existingBooking,
|
||||||
user: {
|
user: {
|
||||||
...existingBooking.user,
|
...existingBooking.user,
|
||||||
email: null,
|
email: null,
|
||||||
},
|
},
|
||||||
paymentRequired: false,
|
paymentRequired: shouldShowPaymentForm,
|
||||||
seatReferenceUid: "",
|
seatReferenceUid: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -574,8 +584,8 @@ async function handler(
|
|||||||
luckyUsers: bookingResponse.userId ? [bookingResponse.userId] : [],
|
luckyUsers: bookingResponse.userId ? [bookingResponse.userId] : [],
|
||||||
isDryRun,
|
isDryRun,
|
||||||
...(isDryRun ? { troubleshooterData } : {}),
|
...(isDryRun ? { troubleshooterData } : {}),
|
||||||
paymentUid: undefined,
|
paymentUid: firstPayment?.uid,
|
||||||
paymentId: undefined,
|
paymentId: firstPayment?.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1430,6 +1440,8 @@ async function handler(
|
|||||||
eventType.schedulingType === SchedulingType.ROUND_ROBIN &&
|
eventType.schedulingType === SchedulingType.ROUND_ROBIN &&
|
||||||
originalRescheduledBooking.userId !== evt.organizer.id;
|
originalRescheduledBooking.userId !== evt.organizer.id;
|
||||||
|
|
||||||
|
const skipDeleteEventsAndMeetings = changedOrganizer;
|
||||||
|
|
||||||
const isBookingRequestedReschedule =
|
const isBookingRequestedReschedule =
|
||||||
!!originalRescheduledBooking &&
|
!!originalRescheduledBooking &&
|
||||||
!!originalRescheduledBooking.rescheduled &&
|
!!originalRescheduledBooking.rescheduled &&
|
||||||
@@ -1702,7 +1714,8 @@ async function handler(
|
|||||||
undefined,
|
undefined,
|
||||||
changedOrganizer,
|
changedOrganizer,
|
||||||
previousHostDestinationCalendar,
|
previousHostDestinationCalendar,
|
||||||
isBookingRequestedReschedule
|
isBookingRequestedReschedule,
|
||||||
|
skipDeleteEventsAndMeetings
|
||||||
);
|
);
|
||||||
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
|
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
|
||||||
// to the default description when we are sending the emails.
|
// to the default description when we are sending the emails.
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
*
|
*
|
||||||
* They don't intend to test what the apps logic should do, but rather test if the apps are called with the correct data. For testing that, once should write tests within each app.
|
* They don't intend to test what the apps logic should do, but rather test if the apps are called with the correct data. For testing that, once should write tests within each app.
|
||||||
*/
|
*/
|
||||||
|
import prismaMock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createBookingScenario,
|
createBookingScenario,
|
||||||
getDate,
|
getDate,
|
||||||
@@ -3277,6 +3279,122 @@ describe("handleNewBooking", () => {
|
|||||||
},
|
},
|
||||||
timeout
|
timeout
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
`Payment retry scenario - should return existing payment UID and prevent duplicate bookings when retrying canceled payment`,
|
||||||
|
async ({ emails }) => {
|
||||||
|
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||||
|
const booker = getBooker({
|
||||||
|
email: "booker@example.com",
|
||||||
|
name: "Booker",
|
||||||
|
});
|
||||||
|
|
||||||
|
const organizer = getOrganizer({
|
||||||
|
name: "Organizer",
|
||||||
|
email: "organizer@example.com",
|
||||||
|
id: 101,
|
||||||
|
schedules: [TestData.schedules.IstWorkHours],
|
||||||
|
credentials: [getGoogleCalendarCredential(), getStripeAppCredential()],
|
||||||
|
selectedCalendars: [TestData.selectedCalendars.google],
|
||||||
|
});
|
||||||
|
|
||||||
|
const scenarioData = getScenarioData({
|
||||||
|
eventTypes: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Paid Event",
|
||||||
|
description: "It's a test Paid Event",
|
||||||
|
slotInterval: 30,
|
||||||
|
requiresConfirmation: false,
|
||||||
|
metadata: {
|
||||||
|
apps: {
|
||||||
|
stripe: {
|
||||||
|
price: 100,
|
||||||
|
enabled: true,
|
||||||
|
currency: "usd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
length: 30,
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
organizer,
|
||||||
|
apps: [TestData.apps["stripe-payment"]],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createBookingScenario(scenarioData);
|
||||||
|
mockSuccessfulVideoMeetingCreation({
|
||||||
|
metadataLookupKey: "dailyvideo",
|
||||||
|
});
|
||||||
|
const { paymentUid } = mockPaymentApp({
|
||||||
|
metadataLookupKey: "stripe",
|
||||||
|
appStoreLookupKey: "stripepayment",
|
||||||
|
});
|
||||||
|
mockCalendarToHaveNoBusySlots("googlecalendar");
|
||||||
|
|
||||||
|
const mockBookingData = getMockRequestDataForBooking({
|
||||||
|
data: {
|
||||||
|
eventTypeId: 1,
|
||||||
|
responses: {
|
||||||
|
email: booker.email,
|
||||||
|
name: booker.name,
|
||||||
|
location: { optionValue: "", value: "New York" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstBooking = await handleNewBooking({
|
||||||
|
bookingData: mockBookingData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBooking).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
paymentUid: expect.any(String),
|
||||||
|
paymentRequired: true,
|
||||||
|
uid: expect.any(String),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstBookingInDb = await prismaMock.booking.findUnique({
|
||||||
|
where: { uid: firstBooking.uid },
|
||||||
|
include: { payment: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBookingInDb).toBeTruthy();
|
||||||
|
expect(firstBookingInDb?.payment).toHaveLength(1);
|
||||||
|
expect(firstBookingInDb?.paid).toBe(false);
|
||||||
|
|
||||||
|
const secondBooking = await handleNewBooking({
|
||||||
|
bookingData: mockBookingData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(secondBooking.uid).toBe(firstBooking.uid);
|
||||||
|
expect(secondBooking.paymentUid).toBe(firstBooking.paymentUid);
|
||||||
|
expect(secondBooking.paymentRequired).toBe(true);
|
||||||
|
|
||||||
|
const bookingsInDb = await prismaMock.booking.findMany({
|
||||||
|
where: {
|
||||||
|
eventTypeId: 1,
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: booker.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { payment: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(bookingsInDb).toHaveLength(1);
|
||||||
|
expect(bookingsInDb[0].payment).toHaveLength(1);
|
||||||
|
expect(bookingsInDb[0].uid).toBe(firstBooking.uid);
|
||||||
|
},
|
||||||
|
timeout
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { shallow } from "zustand/shallow";
|
|||||||
import type { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useEmbedStyles } from "@calcom/embed-core/embed-iframe";
|
import { useEmbedStyles } from "@calcom/embed-core/embed-iframe";
|
||||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth";
|
import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth";
|
||||||
import { daysInMonth, yyyymmdd } from "@calcom/lib/dayjs";
|
import { daysInMonth, yyyymmdd } from "@calcom/lib/dayjs";
|
||||||
import type { IFromUser, IToUser } from "@calcom/lib/getUserAvailability";
|
import type { IFromUser, IToUser } from "@calcom/lib/getUserAvailability";
|
||||||
@@ -227,7 +227,7 @@ const Days = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [selectedDatesAndTimes] = useBookerStore((state) => [state.selectedDatesAndTimes], shallow);
|
const [selectedDatesAndTimes] = useBookerStoreContext((state) => [state.selectedDatesAndTimes], shallow);
|
||||||
|
|
||||||
const isActive = (day: dayjs.Dayjs) => {
|
const isActive = (day: dayjs.Dayjs) => {
|
||||||
// for selecting a range of dates
|
// for selecting a range of dates
|
||||||
@@ -400,7 +400,7 @@ const DatePicker = ({
|
|||||||
minDate && rawBrowsingDate.valueOf() < minDate.valueOf() ? dayjs(minDate) : rawBrowsingDate;
|
minDate && rawBrowsingDate.valueOf() < minDate.valueOf() ? dayjs(minDate) : rawBrowsingDate;
|
||||||
|
|
||||||
const { i18n, t } = useLocale();
|
const { i18n, t } = useLocale();
|
||||||
const bookingData = useBookerStore((state) => state.bookingData);
|
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||||
const isBookingInPast = bookingData ? new Date(bookingData.endTime) < new Date() : false;
|
const isBookingInPast = bookingData ? new Date(bookingData.endTime) < new Date() : false;
|
||||||
const changeMonth = (newMonth: number) => {
|
const changeMonth = (newMonth: number) => {
|
||||||
if (onMonthChange) {
|
if (onMonthChange) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React from "react";
|
|||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { PeriodType } from "@calcom/prisma/enums";
|
import { PeriodType } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { DatePicker } from "../DatePicker";
|
import { DatePicker } from "../DatePicker";
|
||||||
@@ -16,20 +17,22 @@ describe("Tests for DatePicker Component", () => {
|
|||||||
test("Should render correctly with default date", async () => {
|
test("Should render correctly with default date", async () => {
|
||||||
const testDate = dayjs("2024-02-20");
|
const testDate = dayjs("2024-02-20");
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<TooltipProvider>
|
<BookerStoreProvider>
|
||||||
<DatePicker
|
<TooltipProvider>
|
||||||
onChange={noop}
|
<DatePicker
|
||||||
browsingDate={testDate}
|
onChange={noop}
|
||||||
locale="en"
|
browsingDate={testDate}
|
||||||
periodData={{
|
locale="en"
|
||||||
periodType: PeriodType.UNLIMITED,
|
periodData={{
|
||||||
periodDays: null,
|
periodType: PeriodType.UNLIMITED,
|
||||||
periodCountCalendarDays: false,
|
periodDays: null,
|
||||||
periodStartDate: null,
|
periodCountCalendarDays: false,
|
||||||
periodEndDate: null,
|
periodStartDate: null,
|
||||||
}}
|
periodEndDate: null,
|
||||||
/>
|
}}
|
||||||
</TooltipProvider>
|
/>
|
||||||
|
</TooltipProvider>
|
||||||
|
</BookerStoreProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedMonthLabel = getByTestId("selected-month-label");
|
const selectedMonthLabel = getByTestId("selected-month-label");
|
||||||
@@ -40,9 +43,11 @@ describe("Tests for DatePicker Component", () => {
|
|||||||
const testDate = dayjs("2024-02-20");
|
const testDate = dayjs("2024-02-20");
|
||||||
const minDate = dayjs("2025-02-10");
|
const minDate = dayjs("2025-02-10");
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<TooltipProvider>
|
<BookerStoreProvider>
|
||||||
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
|
<TooltipProvider>
|
||||||
</TooltipProvider>
|
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
|
||||||
|
</TooltipProvider>
|
||||||
|
</BookerStoreProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedMonthLabel = getByTestId("selected-month-label");
|
const selectedMonthLabel = getByTestId("selected-month-label");
|
||||||
@@ -53,9 +58,11 @@ describe("Tests for DatePicker Component", () => {
|
|||||||
const testDate = dayjs("2025-03-20");
|
const testDate = dayjs("2025-03-20");
|
||||||
const minDate = dayjs("2025-02-10");
|
const minDate = dayjs("2025-02-10");
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<TooltipProvider>
|
<BookerStoreProvider>
|
||||||
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
|
<TooltipProvider>
|
||||||
</TooltipProvider>
|
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
|
||||||
|
</TooltipProvider>
|
||||||
|
</BookerStoreProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedMonthLabel = getByTestId("selected-month-label");
|
const selectedMonthLabel = getByTestId("selected-month-label");
|
||||||
@@ -85,22 +92,24 @@ describe("Tests for DatePicker Component", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const { getAllByTestId } = render(
|
const { getAllByTestId } = render(
|
||||||
<TooltipProvider>
|
<BookerStoreProvider>
|
||||||
<DatePicker
|
<TooltipProvider>
|
||||||
onChange={noop}
|
<DatePicker
|
||||||
browsingDate={earlyMonthDate}
|
onChange={noop}
|
||||||
locale="en"
|
browsingDate={earlyMonthDate}
|
||||||
slots={slots}
|
locale="en"
|
||||||
isCompact={false}
|
slots={slots}
|
||||||
periodData={{
|
isCompact={false}
|
||||||
periodType: PeriodType.UNLIMITED,
|
periodData={{
|
||||||
periodDays: null,
|
periodType: PeriodType.UNLIMITED,
|
||||||
periodCountCalendarDays: false,
|
periodDays: null,
|
||||||
periodStartDate: null,
|
periodCountCalendarDays: false,
|
||||||
periodEndDate: null,
|
periodStartDate: null,
|
||||||
}}
|
periodEndDate: null,
|
||||||
/>
|
}}
|
||||||
</TooltipProvider>
|
/>
|
||||||
|
</TooltipProvider>
|
||||||
|
</BookerStoreProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const dayElements = getAllByTestId("day");
|
const dayElements = getAllByTestId("day");
|
||||||
@@ -127,22 +136,24 @@ describe("Tests for DatePicker Component", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const { getAllByTestId, queryByText } = render(
|
const { getAllByTestId, queryByText } = render(
|
||||||
<TooltipProvider>
|
<BookerStoreProvider>
|
||||||
<DatePicker
|
<TooltipProvider>
|
||||||
onChange={noop}
|
<DatePicker
|
||||||
browsingDate={lateMonthDate}
|
onChange={noop}
|
||||||
locale="en"
|
browsingDate={lateMonthDate}
|
||||||
slots={slots}
|
locale="en"
|
||||||
isCompact={false}
|
slots={slots}
|
||||||
periodData={{
|
isCompact={false}
|
||||||
periodType: PeriodType.UNLIMITED,
|
periodData={{
|
||||||
periodDays: null,
|
periodType: PeriodType.UNLIMITED,
|
||||||
periodCountCalendarDays: false,
|
periodDays: null,
|
||||||
periodStartDate: null,
|
periodCountCalendarDays: false,
|
||||||
periodEndDate: null,
|
periodStartDate: null,
|
||||||
}}
|
periodEndDate: null,
|
||||||
/>
|
}}
|
||||||
</TooltipProvider>
|
/>
|
||||||
|
</TooltipProvider>
|
||||||
|
</BookerStoreProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const dayElements = getAllByTestId("day");
|
const dayElements = getAllByTestId("day");
|
||||||
@@ -165,22 +176,24 @@ describe("Tests for DatePicker Component", () => {
|
|||||||
const slots = createMockSlots(["2024-01-25", "2024-02-01"]);
|
const slots = createMockSlots(["2024-01-25", "2024-02-01"]);
|
||||||
|
|
||||||
const { getAllByTestId } = render(
|
const { getAllByTestId } = render(
|
||||||
<TooltipProvider>
|
<BookerStoreProvider>
|
||||||
<DatePicker
|
<TooltipProvider>
|
||||||
onChange={noop}
|
<DatePicker
|
||||||
browsingDate={lateMonthDate}
|
onChange={noop}
|
||||||
locale="en"
|
browsingDate={lateMonthDate}
|
||||||
slots={slots}
|
locale="en"
|
||||||
isCompact={true} // This should force traditional view
|
slots={slots}
|
||||||
periodData={{
|
isCompact={true} // This should force traditional view
|
||||||
periodType: PeriodType.UNLIMITED,
|
periodData={{
|
||||||
periodDays: null,
|
periodType: PeriodType.UNLIMITED,
|
||||||
periodCountCalendarDays: false,
|
periodDays: null,
|
||||||
periodStartDate: null,
|
periodCountCalendarDays: false,
|
||||||
periodEndDate: null,
|
periodStartDate: null,
|
||||||
}}
|
periodEndDate: null,
|
||||||
/>
|
}}
|
||||||
</TooltipProvider>
|
/>
|
||||||
|
</TooltipProvider>
|
||||||
|
</BookerStoreProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const dayElements = getAllByTestId("day");
|
const dayElements = getAllByTestId("day");
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import removeMember from "@calcom/features/ee/teams/lib/removeMember";
|
import { TeamService } from "@calcom/lib/server/service/teamService";
|
||||||
|
|
||||||
const removeUserFromOrg = async ({ userId, orgId }: { userId: number; orgId: number }) => {
|
const removeUserFromOrg = async ({ userId, orgId }: { userId: number; orgId: number }) => {
|
||||||
return removeMember({
|
return TeamService.removeMembers({ teamIds: [orgId], userIds: [userId], isOrg: true });
|
||||||
memberId: userId,
|
|
||||||
teamId: orgId,
|
|
||||||
isOrg: true,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default removeUserFromOrg;
|
export default removeUserFromOrg;
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* Tests for calendar event deletion during round robin organizer change operations.
|
||||||
|
* Covers scenarios where events need to be deleted from original hosts and
|
||||||
|
* credential handling during organizer changes.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
getDate,
|
||||||
|
createBookingScenario,
|
||||||
|
getScenarioData,
|
||||||
|
getMockBookingAttendee,
|
||||||
|
TestData,
|
||||||
|
getOrganizer,
|
||||||
|
getGoogleCalendarCredential,
|
||||||
|
mockCalendarToHaveNoBusySlots,
|
||||||
|
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||||
|
import {
|
||||||
|
expectBookingToBeInDatabase,
|
||||||
|
expectSuccessfulRoundRobinReschedulingEmails,
|
||||||
|
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||||
|
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||||
|
|
||||||
|
import { describe, expect } from "vitest";
|
||||||
|
|
||||||
|
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||||
|
import { SchedulingType, BookingStatus } from "@calcom/prisma/enums";
|
||||||
|
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||||
|
|
||||||
|
describe("roundRobinReassignment test", () => {
|
||||||
|
setupAndTeardown();
|
||||||
|
|
||||||
|
test("should delete calendar events from original host when round robin reassignment changes organizer", async ({
|
||||||
|
emails,
|
||||||
|
}) => {
|
||||||
|
const roundRobinReassignment = (await import("./roundRobinReassignment")).default;
|
||||||
|
|
||||||
|
const originalHost = getOrganizer({
|
||||||
|
name: "Original Host",
|
||||||
|
email: "originalhost@example.com",
|
||||||
|
id: 101,
|
||||||
|
schedules: [TestData.schedules.IstWorkHours],
|
||||||
|
credentials: [getGoogleCalendarCredential()],
|
||||||
|
selectedCalendars: [TestData.selectedCalendars.google],
|
||||||
|
});
|
||||||
|
|
||||||
|
const newHost = getOrganizer({
|
||||||
|
name: "New Host",
|
||||||
|
email: "newhost@example.com",
|
||||||
|
id: 102,
|
||||||
|
schedules: [TestData.schedules.IstWorkHours],
|
||||||
|
credentials: [getGoogleCalendarCredential()],
|
||||||
|
selectedCalendars: [TestData.selectedCalendars.google],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { dateString: dateStringPlusOne } = getDate({ dateIncrement: 1 });
|
||||||
|
const bookingToReassignUid = "booking-to-reassign-with-deletion";
|
||||||
|
|
||||||
|
await createBookingScenario(
|
||||||
|
getScenarioData({
|
||||||
|
eventTypes: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
slug: "round-robin-event",
|
||||||
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||||
|
length: 45,
|
||||||
|
users: [{ id: 101 }, { id: 102 }],
|
||||||
|
hosts: [
|
||||||
|
{ userId: 101, isFixed: false },
|
||||||
|
{ userId: 102, isFixed: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bookings: [
|
||||||
|
{
|
||||||
|
id: 123,
|
||||||
|
eventTypeId: 1,
|
||||||
|
userId: originalHost.id,
|
||||||
|
uid: bookingToReassignUid,
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
startTime: `${dateStringPlusOne}T05:00:00.000Z`,
|
||||||
|
endTime: `${dateStringPlusOne}T05:45:00.000Z`,
|
||||||
|
attendees: [
|
||||||
|
getMockBookingAttendee({
|
||||||
|
id: 2,
|
||||||
|
name: "attendee",
|
||||||
|
email: "attendee@test.com",
|
||||||
|
locale: "en",
|
||||||
|
timeZone: "Asia/Kolkata",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
references: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: appStoreMetadata.googlecalendar.type,
|
||||||
|
uid: "ORIGINAL_EVENT_ID",
|
||||||
|
meetingId: "ORIGINAL_EVENT_ID",
|
||||||
|
meetingPassword: null,
|
||||||
|
meetingUrl: null,
|
||||||
|
externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID",
|
||||||
|
credentialId: 1,
|
||||||
|
deleted: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
organizer: originalHost,
|
||||||
|
usersApartFromOrganizer: [newHost],
|
||||||
|
apps: [TestData.apps["google-calendar"]],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||||
|
create: { uid: "NEW_EVENT_ID" },
|
||||||
|
update: { uid: "UPDATED_EVENT_ID" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await roundRobinReassignment({
|
||||||
|
bookingId: 123,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that calendar deletion occurred (may be called multiple times due to duplicate references)
|
||||||
|
expect(calendarMock.deleteEventCalls.length).toBeGreaterThanOrEqual(1);
|
||||||
|
const deleteCall = calendarMock.deleteEventCalls[0];
|
||||||
|
expect(deleteCall.args.uid).toBe("ORIGINAL_EVENT_ID");
|
||||||
|
expect(deleteCall.args.externalCalendarId).toBe("MOCK_EXTERNAL_CALENDAR_ID");
|
||||||
|
expect(deleteCall.args.event.organizer.email).toBe(newHost.email); // Current implementation uses new host credentials
|
||||||
|
expect(deleteCall.args.event.uid).toBe(bookingToReassignUid);
|
||||||
|
|
||||||
|
// Verify that creation occurred with new host credentials
|
||||||
|
expect(calendarMock.createEventCalls.length).toBe(1);
|
||||||
|
const createCall = calendarMock.createEventCalls[0];
|
||||||
|
expect(createCall.args.calEvent.organizer.email).toBe(newHost.email);
|
||||||
|
|
||||||
|
// Verify the booking was reassigned to the new host
|
||||||
|
expectBookingToBeInDatabase({
|
||||||
|
uid: bookingToReassignUid,
|
||||||
|
userId: newHost.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectSuccessfulRoundRobinReschedulingEmails({
|
||||||
|
prevOrganizer: originalHost,
|
||||||
|
newOrganizer: newHost,
|
||||||
|
emails,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import logger from "@calcom/lib/logger";
|
|
||||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
|
|
||||||
import { deleteWorkfowRemindersOfRemovedMember } from "./deleteWorkflowRemindersOfRemovedMember";
|
|
||||||
|
|
||||||
const log = logger.getSubLogger({ prefix: ["removeMember"] });
|
|
||||||
|
|
||||||
const removeMember = async ({
|
|
||||||
memberId,
|
|
||||||
teamId,
|
|
||||||
isOrg,
|
|
||||||
}: {
|
|
||||||
memberId: number;
|
|
||||||
teamId: number;
|
|
||||||
isOrg: boolean;
|
|
||||||
}) => {
|
|
||||||
const [membership] = await prisma.$transaction([
|
|
||||||
prisma.membership.delete({
|
|
||||||
where: {
|
|
||||||
userId_teamId: { userId: memberId, teamId: teamId },
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
user: true,
|
|
||||||
team: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
// remove user as host from team events associated with this membership
|
|
||||||
prisma.host.deleteMany({
|
|
||||||
where: {
|
|
||||||
userId: memberId,
|
|
||||||
eventType: {
|
|
||||||
teamId: teamId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const team = await prisma.team.findUnique({
|
|
||||||
where: { id: teamId },
|
|
||||||
select: {
|
|
||||||
isOrganization: true,
|
|
||||||
organizationSettings: true,
|
|
||||||
id: true,
|
|
||||||
metadata: true,
|
|
||||||
activeOrgWorkflows: true,
|
|
||||||
parentId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const foundUser = await prisma.user.findUnique({
|
|
||||||
where: { id: memberId },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
movedToProfileId: true,
|
|
||||||
email: true,
|
|
||||||
username: true,
|
|
||||||
completedOnboarding: true,
|
|
||||||
teams: {
|
|
||||||
select: {
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
parentId: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!team || !foundUser) throw new TRPCError({ code: "NOT_FOUND" });
|
|
||||||
|
|
||||||
if (isOrg) {
|
|
||||||
log.debug("Removing a member from the organization");
|
|
||||||
// Deleting membership from all child teams
|
|
||||||
// Delete all sub-team memberships where this team is the organization
|
|
||||||
await prisma.membership.deleteMany({
|
|
||||||
where: {
|
|
||||||
team: {
|
|
||||||
parentId: teamId,
|
|
||||||
},
|
|
||||||
userId: membership.userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const userToDeleteMembershipOf = foundUser;
|
|
||||||
|
|
||||||
const profileToDelete = await ProfileRepository.findByUserIdAndOrgId({
|
|
||||||
userId: userToDeleteMembershipOf.id,
|
|
||||||
organizationId: team.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
userToDeleteMembershipOf.username &&
|
|
||||||
userToDeleteMembershipOf.movedToProfileId === profileToDelete?.id
|
|
||||||
) {
|
|
||||||
log.debug("Cleaning up tempOrgRedirect for user", userToDeleteMembershipOf.username);
|
|
||||||
|
|
||||||
await prisma.tempOrgRedirect.deleteMany({
|
|
||||||
where: {
|
|
||||||
from: userToDeleteMembershipOf.username,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$transaction([
|
|
||||||
prisma.user.update({
|
|
||||||
where: { id: membership.userId },
|
|
||||||
data: { organizationId: null },
|
|
||||||
}),
|
|
||||||
ProfileRepository.delete({
|
|
||||||
userId: membership.userId,
|
|
||||||
organizationId: team.id,
|
|
||||||
}),
|
|
||||||
prisma.host.deleteMany({
|
|
||||||
where: {
|
|
||||||
userId: memberId,
|
|
||||||
eventType: {
|
|
||||||
team: {
|
|
||||||
parentId: teamId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deleted managed event types from this team from this member
|
|
||||||
await prisma.eventType.deleteMany({
|
|
||||||
where: { parent: { teamId: teamId }, userId: membership.userId },
|
|
||||||
});
|
|
||||||
|
|
||||||
await deleteWorkfowRemindersOfRemovedMember(team, memberId, isOrg);
|
|
||||||
|
|
||||||
return { membership };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default removeMember;
|
|
||||||
@@ -11,7 +11,12 @@ import { shallow } from "zustand/shallow";
|
|||||||
import type { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { AvailableTimes, AvailableTimesHeader } from "@calcom/features/bookings";
|
import { AvailableTimes, AvailableTimesHeader } from "@calcom/features/bookings";
|
||||||
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
|
import {
|
||||||
|
BookerStoreProvider,
|
||||||
|
useInitializeBookerStoreContext,
|
||||||
|
useBookerStoreContext,
|
||||||
|
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
|
import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||||
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
|
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
|
||||||
import DatePicker from "@calcom/features/calendars/DatePicker";
|
import DatePicker from "@calcom/features/calendars/DatePicker";
|
||||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||||
@@ -260,13 +265,21 @@ const EmailEmbed = ({
|
|||||||
org: orgSlug,
|
org: orgSlug,
|
||||||
isTeamEvent,
|
isTeamEvent,
|
||||||
});
|
});
|
||||||
|
useInitializeBookerStoreContext({
|
||||||
|
username,
|
||||||
|
eventSlug: eventType?.slug ?? "",
|
||||||
|
eventId: eventType?.id,
|
||||||
|
layout: BookerLayouts.MONTH_VIEW,
|
||||||
|
org: orgSlug,
|
||||||
|
isTeamEvent,
|
||||||
|
});
|
||||||
|
|
||||||
const [month, selectedDate, selectedDatesAndTimes] = useBookerStore(
|
const [month, selectedDate, selectedDatesAndTimes] = useBookerStoreContext(
|
||||||
(state) => [state.month, state.selectedDate, state.selectedDatesAndTimes],
|
(state) => [state.month, state.selectedDate, state.selectedDatesAndTimes],
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot, setTimezone] =
|
const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot, setTimezone] =
|
||||||
useBookerStore(
|
useBookerStoreContext(
|
||||||
(state) => [
|
(state) => [
|
||||||
state.setSelectedDate,
|
state.setSelectedDate,
|
||||||
state.setMonth,
|
state.setMonth,
|
||||||
@@ -686,7 +699,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
const emailContentRef = useRef<HTMLDivElement>(null);
|
const emailContentRef = useRef<HTMLDivElement>(null);
|
||||||
const { data } = useSession();
|
const { data } = useSession();
|
||||||
|
|
||||||
const [month, selectedDatesAndTimes] = useBookerStore(
|
const [month, selectedDatesAndTimes] = useBookerStoreContext(
|
||||||
(state) => [state.month, state.selectedDatesAndTimes],
|
(state) => [state.month, state.selectedDatesAndTimes],
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
@@ -1369,32 +1382,34 @@ export const EmbedDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<BookerStoreProvider>
|
||||||
{...(noQueryParamMode
|
<Dialog
|
||||||
? {
|
{...(noQueryParamMode
|
||||||
open: embedState !== null,
|
? {
|
||||||
onOpenChange: (open) => !open && handleDialogClose(),
|
open: embedState !== null,
|
||||||
}
|
onOpenChange: (open) => !open && handleDialogClose(),
|
||||||
: {
|
}
|
||||||
// Must not set name when noQueryParam mode as required by Dialog component
|
: {
|
||||||
name: "embed",
|
// Must not set name when noQueryParam mode as required by Dialog component
|
||||||
clearQueryParamsOnClose: queryParamsForDialog,
|
name: "embed",
|
||||||
})}>
|
clearQueryParamsOnClose: queryParamsForDialog,
|
||||||
{!embedParams.embedType ? (
|
})}>
|
||||||
<ChooseEmbedTypesDialogContent types={types} noQueryParamMode={noQueryParamMode} />
|
{!embedParams.embedType ? (
|
||||||
) : (
|
<ChooseEmbedTypesDialogContent types={types} noQueryParamMode={noQueryParamMode} />
|
||||||
<EmbedTypeCodeAndPreviewDialogContent
|
) : (
|
||||||
embedType={embedParams.embedType as EmbedType}
|
<EmbedTypeCodeAndPreviewDialogContent
|
||||||
embedUrl={embedParams.embedUrl}
|
embedType={embedParams.embedType as EmbedType}
|
||||||
namespace={embedParams.namespace}
|
embedUrl={embedParams.embedUrl}
|
||||||
tabs={tabs}
|
namespace={embedParams.namespace}
|
||||||
types={types}
|
tabs={tabs}
|
||||||
eventTypeHideOptionDisabled={eventTypeHideOptionDisabled}
|
types={types}
|
||||||
defaultBrandColor={defaultBrandColor}
|
eventTypeHideOptionDisabled={eventTypeHideOptionDisabled}
|
||||||
noQueryParamMode={noQueryParamMode}
|
defaultBrandColor={defaultBrandColor}
|
||||||
/>
|
noQueryParamMode={noQueryParamMode}
|
||||||
)}
|
/>
|
||||||
</Dialog>
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</BookerStoreProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -110,13 +110,15 @@ describe("RoleManagementFactory", () => {
|
|||||||
it("should allow role change when user has permission", async () => {
|
it("should allow role change when user has permission", async () => {
|
||||||
mockPermissionCheckService.checkPermission.mockResolvedValue(true);
|
mockPermissionCheckService.checkPermission.mockResolvedValue(true);
|
||||||
const manager = await factory.createRoleManager(organizationId);
|
const manager = await factory.createRoleManager(organizationId);
|
||||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).resolves.not.toThrow();
|
await expect(
|
||||||
|
manager.checkPermissionToChangeRole(userId, organizationId, "org")
|
||||||
|
).resolves.not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw UNAUTHORIZED when user lacks permission", async () => {
|
it("should throw UNAUTHORIZED when user lacks permission", async () => {
|
||||||
mockPermissionCheckService.checkPermission.mockResolvedValue(false);
|
mockPermissionCheckService.checkPermission.mockResolvedValue(false);
|
||||||
const manager = await factory.createRoleManager(organizationId);
|
const manager = await factory.createRoleManager(organizationId);
|
||||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(
|
await expect(manager.checkPermissionToChangeRole(userId, organizationId, "org")).rejects.toThrow(
|
||||||
new RoleManagementError(
|
new RoleManagementError(
|
||||||
"You do not have permission to change roles",
|
"You do not have permission to change roles",
|
||||||
RoleManagementErrorCode.UNAUTHORIZED
|
RoleManagementErrorCode.UNAUTHORIZED
|
||||||
@@ -193,13 +195,15 @@ describe("RoleManagementFactory", () => {
|
|||||||
customRoleId: null,
|
customRoleId: null,
|
||||||
});
|
});
|
||||||
const manager = await factory.createRoleManager(organizationId);
|
const manager = await factory.createRoleManager(organizationId);
|
||||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).resolves.not.toThrow();
|
await expect(
|
||||||
|
manager.checkPermissionToChangeRole(userId, organizationId, "org")
|
||||||
|
).resolves.not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw UNAUTHORIZED when user is not owner", async () => {
|
it("should throw UNAUTHORIZED when user is not owner", async () => {
|
||||||
vi.mocked(isOrganisationAdmin).mockResolvedValue(false);
|
vi.mocked(isOrganisationAdmin).mockResolvedValue(false);
|
||||||
const manager = await factory.createRoleManager(organizationId);
|
const manager = await factory.createRoleManager(organizationId);
|
||||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(
|
await expect(manager.checkPermissionToChangeRole(userId, organizationId, "org")).rejects.toThrow(
|
||||||
new RoleManagementError(
|
new RoleManagementError(
|
||||||
"Only owners or admin can update roles",
|
"Only owners or admin can update roles",
|
||||||
RoleManagementErrorCode.UNAUTHORIZED
|
RoleManagementErrorCode.UNAUTHORIZED
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||||
|
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ import { RoleService } from "./role.service";
|
|||||||
|
|
||||||
interface IRoleManager {
|
interface IRoleManager {
|
||||||
isPBACEnabled: boolean;
|
isPBACEnabled: boolean;
|
||||||
checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void>;
|
checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void>;
|
||||||
assignRole(
|
assignRole(
|
||||||
userId: number,
|
userId: number,
|
||||||
organizationId: number,
|
organizationId: number,
|
||||||
@@ -29,11 +30,11 @@ class PBACRoleManager implements IRoleManager {
|
|||||||
private readonly permissionCheckService: PermissionCheckService
|
private readonly permissionCheckService: PermissionCheckService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void> {
|
async checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void> {
|
||||||
const hasPermission = await this.permissionCheckService.checkPermission({
|
const hasPermission = await this.permissionCheckService.checkPermission({
|
||||||
userId,
|
userId,
|
||||||
teamId: organizationId,
|
teamId: targetId,
|
||||||
permission: "organization.changeMemberRole",
|
permission: scope === "team" ? "team.changeMemberRole" : "organization.changeMemberRole",
|
||||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,9 +96,11 @@ class PBACRoleManager implements IRoleManager {
|
|||||||
|
|
||||||
class LegacyRoleManager implements IRoleManager {
|
class LegacyRoleManager implements IRoleManager {
|
||||||
public isPBACEnabled = false;
|
public isPBACEnabled = false;
|
||||||
async checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void> {
|
async checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void> {
|
||||||
const membership = await isOrganisationAdmin(userId, organizationId);
|
const membership =
|
||||||
|
scope === "team"
|
||||||
|
? !!(await isTeamAdmin(userId, targetId))
|
||||||
|
: !!(await isOrganisationAdmin(userId, targetId));
|
||||||
// Only OWNER/ADMIN can update role
|
// Only OWNER/ADMIN can update role
|
||||||
if (!membership) {
|
if (!membership) {
|
||||||
throw new RoleManagementError(
|
throw new RoleManagementError(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||||
import classNames from "@calcom/ui/classNames";
|
import classNames from "@calcom/ui/classNames";
|
||||||
|
|
||||||
@@ -63,5 +64,9 @@ const TroubleshooterComponent = ({ month }: TroubleshooterProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Troubleshooter = ({ month }: TroubleshooterProps) => {
|
export const Troubleshooter = ({ month }: TroubleshooterProps) => {
|
||||||
return <TroubleshooterComponent month={month} />;
|
return (
|
||||||
|
<BookerStoreProvider>
|
||||||
|
<TroubleshooterComponent month={month} />
|
||||||
|
</BookerStoreProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -570,7 +570,8 @@ export default class EventManager {
|
|||||||
newBookingId?: number,
|
newBookingId?: number,
|
||||||
changedOrganizer?: boolean,
|
changedOrganizer?: boolean,
|
||||||
previousHostDestinationCalendar?: DestinationCalendar[] | null,
|
previousHostDestinationCalendar?: DestinationCalendar[] | null,
|
||||||
isBookingRequestedReschedule?: boolean
|
isBookingRequestedReschedule?: boolean,
|
||||||
|
skipDeleteEventsAndMeetings?: boolean
|
||||||
): Promise<CreateUpdateResult> {
|
): Promise<CreateUpdateResult> {
|
||||||
const originalEvt = processLocation(event);
|
const originalEvt = processLocation(event);
|
||||||
const evt = cloneDeep(originalEvt);
|
const evt = cloneDeep(originalEvt);
|
||||||
@@ -637,7 +638,7 @@ export default class EventManager {
|
|||||||
const shouldUpdateBookingReferences =
|
const shouldUpdateBookingReferences =
|
||||||
!!changedOrganizer || isLocationChanged || !!isBookingRequestedReschedule || isDailyVideoRoomExpired;
|
!!changedOrganizer || isLocationChanged || !!isBookingRequestedReschedule || isDailyVideoRoomExpired;
|
||||||
|
|
||||||
if (evt.requiresConfirmation && !changedOrganizer) {
|
if (evt.requiresConfirmation && !skipDeleteEventsAndMeetings) {
|
||||||
log.debug("RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking");
|
log.debug("RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking");
|
||||||
// As the reschedule requires confirmation, we can't update the events and meetings to new time yet. So, just delete them and let it be handled when organizer confirms the booking.
|
// As the reschedule requires confirmation, we can't update the events and meetings to new time yet. So, just delete them and let it be handled when organizer confirms the booking.
|
||||||
await this.deleteEventsAndMeetings({
|
await this.deleteEventsAndMeetings({
|
||||||
@@ -646,6 +647,14 @@ export default class EventManager {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (changedOrganizer) {
|
if (changedOrganizer) {
|
||||||
|
if (!skipDeleteEventsAndMeetings) {
|
||||||
|
log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking");
|
||||||
|
await this.deleteEventsAndMeetings({
|
||||||
|
event: { ...event, destinationCalendar: previousHostDestinationCalendar },
|
||||||
|
bookingReferences: booking.references,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
log.debug("RescheduleOrganizerChanged: Creating Event and Meeting for for new booking");
|
log.debug("RescheduleOrganizerChanged: Creating Event and Meeting for for new booking");
|
||||||
const createdEvent = await this.create(originalEvt);
|
const createdEvent = await this.create(originalEvt);
|
||||||
results.push(...createdEvent.results);
|
results.push(...createdEvent.results);
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export const IS_CALCOM =
|
|||||||
(new URL(WEBAPP_URL).hostname.endsWith("cal.com") ||
|
(new URL(WEBAPP_URL).hostname.endsWith("cal.com") ||
|
||||||
new URL(WEBAPP_URL).hostname.endsWith("cal.dev") ||
|
new URL(WEBAPP_URL).hostname.endsWith("cal.dev") ||
|
||||||
new URL(WEBAPP_URL).hostname.endsWith("cal.qa") ||
|
new URL(WEBAPP_URL).hostname.endsWith("cal.qa") ||
|
||||||
new URL(WEBAPP_URL).hostname.endsWith("cal-staging.com"));
|
new URL(WEBAPP_URL).hostname.endsWith("cal-staging.com") ||
|
||||||
|
new URL(WEBAPP_URL).hostname.endsWith("cal.eu"));
|
||||||
|
|
||||||
export const CONSOLE_URL =
|
export const CONSOLE_URL =
|
||||||
new URL(WEBAPP_URL).hostname.endsWith(".cal.dev") ||
|
new URL(WEBAPP_URL).hostname.endsWith(".cal.dev") ||
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { eventTypeRepositoryModule } from "../modules/EventType";
|
|||||||
import { featuresRepositoryModule } from "../modules/Features";
|
import { featuresRepositoryModule } from "../modules/Features";
|
||||||
import { filterHostsModule } from "../modules/FilterHosts";
|
import { filterHostsModule } from "../modules/FilterHosts";
|
||||||
import { getUserAvailabilityModule } from "../modules/GetUserAvailability";
|
import { getUserAvailabilityModule } from "../modules/GetUserAvailability";
|
||||||
|
import { membershipRepositoryModule } from "../modules/Membership";
|
||||||
|
import { noSlotsNotificationModule } from "../modules/NoSlotsNotification";
|
||||||
import { oooRepositoryModule } from "../modules/Ooo";
|
import { oooRepositoryModule } from "../modules/Ooo";
|
||||||
import { qualifiedHostsModule } from "../modules/QualifiedHosts";
|
import { qualifiedHostsModule } from "../modules/QualifiedHosts";
|
||||||
import { routingFormResponseRepositoryModule } from "../modules/RoutingFormResponse";
|
import { routingFormResponseRepositoryModule } from "../modules/RoutingFormResponse";
|
||||||
@@ -29,6 +31,7 @@ container.load(DI_TOKENS.OOO_REPOSITORY_MODULE, oooRepositoryModule);
|
|||||||
container.load(DI_TOKENS.SCHEDULE_REPOSITORY_MODULE, scheduleRepositoryModule);
|
container.load(DI_TOKENS.SCHEDULE_REPOSITORY_MODULE, scheduleRepositoryModule);
|
||||||
container.load(DI_TOKENS.SELECTED_SLOT_REPOSITORY_MODULE, selectedSlotsRepositoryModule);
|
container.load(DI_TOKENS.SELECTED_SLOT_REPOSITORY_MODULE, selectedSlotsRepositoryModule);
|
||||||
container.load(DI_TOKENS.TEAM_REPOSITORY_MODULE, teamRepositoryModule);
|
container.load(DI_TOKENS.TEAM_REPOSITORY_MODULE, teamRepositoryModule);
|
||||||
|
container.load(DI_TOKENS.MEMBERSHIP_REPOSITORY_MODULE, membershipRepositoryModule);
|
||||||
container.load(DI_TOKENS.USER_REPOSITORY_MODULE, userRepositoryModule);
|
container.load(DI_TOKENS.USER_REPOSITORY_MODULE, userRepositoryModule);
|
||||||
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
|
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
|
||||||
container.load(DI_TOKENS.EVENT_TYPE_REPOSITORY_MODULE, eventTypeRepositoryModule);
|
container.load(DI_TOKENS.EVENT_TYPE_REPOSITORY_MODULE, eventTypeRepositoryModule);
|
||||||
@@ -42,6 +45,7 @@ container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
|
|||||||
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
|
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
|
||||||
container.load(DI_TOKENS.FILTER_HOSTS_SERVICE_MODULE, filterHostsModule);
|
container.load(DI_TOKENS.FILTER_HOSTS_SERVICE_MODULE, filterHostsModule);
|
||||||
container.load(DI_TOKENS.QUALIFIED_HOSTS_SERVICE_MODULE, qualifiedHostsModule);
|
container.load(DI_TOKENS.QUALIFIED_HOSTS_SERVICE_MODULE, qualifiedHostsModule);
|
||||||
|
container.load(DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE_MODULE, noSlotsNotificationModule);
|
||||||
|
|
||||||
export function getAvailableSlotsService() {
|
export function getAvailableSlotsService() {
|
||||||
return container.get<AvailableSlotsService>(DI_TOKENS.AVAILABLE_SLOTS_SERVICE);
|
return container.get<AvailableSlotsService>(DI_TOKENS.AVAILABLE_SLOTS_SERVICE);
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import type {
|
|||||||
InsightsRoutingServicePublicOptions,
|
InsightsRoutingServicePublicOptions,
|
||||||
InsightsRoutingServiceFilterOptions,
|
InsightsRoutingServiceFilterOptions,
|
||||||
InsightsRoutingBaseService,
|
InsightsRoutingBaseService,
|
||||||
} from "@calcom/lib/server/service/insightsRoutingBase";
|
} from "@calcom/lib/server/service/InsightsRoutingBaseService";
|
||||||
import type { InsightsRoutingService } from "@calcom/lib/server/service/insightsRoutingDI";
|
import type { InsightsRoutingService } from "@calcom/lib/server/service/InsightsRoutingDIService";
|
||||||
import { prismaModule } from "@calcom/prisma/prisma.module";
|
import { prismaModule } from "@calcom/prisma/prisma.module";
|
||||||
|
|
||||||
import { insightsRoutingModule } from "../modules/InsightsRouting";
|
import { insightsRoutingModule } from "../modules/InsightsRouting";
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { createContainer } from "@evyweb/ioctopus";
|
||||||
|
|
||||||
|
import { redisModule } from "@calcom/features/redis/di/redisModule";
|
||||||
|
import { DI_TOKENS } from "@calcom/lib/di/tokens";
|
||||||
|
import { prismaModule } from "@calcom/prisma/prisma.module";
|
||||||
|
import type { NoSlotsNotificationService } from "@calcom/trpc/server/routers/viewer/slots/handleNotificationWhenNoSlots";
|
||||||
|
|
||||||
|
import { membershipRepositoryModule } from "../modules/Membership";
|
||||||
|
import { noSlotsNotificationModule } from "../modules/NoSlotsNotification";
|
||||||
|
import { teamRepositoryModule } from "../modules/Team";
|
||||||
|
|
||||||
|
const container = createContainer();
|
||||||
|
container.load(DI_TOKENS.REDIS_CLIENT, redisModule);
|
||||||
|
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
|
||||||
|
container.load(DI_TOKENS.TEAM_REPOSITORY_MODULE, teamRepositoryModule);
|
||||||
|
container.load(DI_TOKENS.MEMBERSHIP_REPOSITORY_MODULE, membershipRepositoryModule);
|
||||||
|
container.load(DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE_MODULE, noSlotsNotificationModule);
|
||||||
|
|
||||||
|
export function getNoSlotsNotificationService() {
|
||||||
|
return container.get<NoSlotsNotificationService>(DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE);
|
||||||
|
}
|
||||||
@@ -22,4 +22,5 @@ availableSlotsModule.bind(DI_TOKENS.AVAILABLE_SLOTS_SERVICE).toClass(AvailableSl
|
|||||||
busyTimesService: DI_TOKENS.BUSY_TIMES_SERVICE,
|
busyTimesService: DI_TOKENS.BUSY_TIMES_SERVICE,
|
||||||
featuresRepo: DI_TOKENS.FEATURES_REPOSITORY,
|
featuresRepo: DI_TOKENS.FEATURES_REPOSITORY,
|
||||||
qualifiedHostsService: DI_TOKENS.QUALIFIED_HOSTS_SERVICE,
|
qualifiedHostsService: DI_TOKENS.QUALIFIED_HOSTS_SERVICE,
|
||||||
|
noSlotsNotificationService: DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE,
|
||||||
} satisfies Record<keyof IAvailableSlotsService, symbol>);
|
} satisfies Record<keyof IAvailableSlotsService, symbol>);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createModule } from "@evyweb/ioctopus";
|
import { createModule } from "@evyweb/ioctopus";
|
||||||
|
|
||||||
import type { IInsightsRoutingService } from "@calcom/lib/server/service/insightsRoutingDI";
|
import type { IInsightsRoutingService } from "@calcom/lib/server/service/InsightsRoutingDIService";
|
||||||
import { InsightsRoutingService } from "@calcom/lib/server/service/insightsRoutingDI";
|
import { InsightsRoutingService } from "@calcom/lib/server/service/InsightsRoutingDIService";
|
||||||
|
|
||||||
import { DI_TOKENS } from "../tokens";
|
import { DI_TOKENS } from "../tokens";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createModule } from "@evyweb/ioctopus";
|
||||||
|
|
||||||
|
import { DI_TOKENS } from "@calcom/lib/di/tokens";
|
||||||
|
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||||
|
|
||||||
|
export const membershipRepositoryModule = createModule();
|
||||||
|
membershipRepositoryModule
|
||||||
|
.bind(DI_TOKENS.MEMBERSHIP_REPOSITORY)
|
||||||
|
.toClass(MembershipRepository, [DI_TOKENS.PRISMA_CLIENT]);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user