Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3fb7340aa | ||
|
|
e4ef2832cd | ||
|
|
881602cf1c | ||
|
|
4c01f171ae | ||
|
|
d9817fcb07 | ||
|
|
a784af3e47 | ||
|
|
c268925330 | ||
|
|
378e8e0330 | ||
|
|
8b3fbb672b | ||
|
|
2d5a86774e | ||
|
|
0865104a77 | ||
|
|
1914b37ee4 | ||
|
|
58ef0abb99 | ||
|
|
13ba680a73 | ||
|
|
f40c6da93b | ||
|
|
fc2e81e89a | ||
|
|
293dd1a140 | ||
|
|
b7fda31678 | ||
|
|
94a04f0dc2 | ||
|
|
2338f85dcf | ||
|
|
fbaccc8e5f | ||
|
|
750db38d38 |
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
Added new callback functions to the handleFormSubmit method in the EventTypeSettings and AvailabilitySettings atoms. The handleFormSubmit method now accepts an optional callbacks object with the following properties:
|
||||
|
||||
- **onSuccess**: Called when the form submission is successful, allowing additional logic to be executed after the update.
|
||||
|
||||
- **onError**: Called when an error occurs during form submission, providing details about the error to handle specific cases or display custom messages.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
feat: style calendar settings and availability overrides
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
booker atom: allow toggling org and team info when booking round robin
|
||||
Vendored
@@ -33,7 +33,7 @@
|
||||
"@sentry/nextjs": "^9.15.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"memory-cache": "^0.2.0",
|
||||
"next": "^15.3.0",
|
||||
"next": "^15.4.5",
|
||||
"next-api-middleware": "^1.0.1",
|
||||
"next-axiom": "^0.17.0",
|
||||
"next-swagger-doc": "^0.3.6",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.300",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.306",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
+10
-31
@@ -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 { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Patch,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags, OmitType } from "@nestjs/swagger";
|
||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import {
|
||||
EVENT_TYPE_READ,
|
||||
EVENT_TYPE_WRITE,
|
||||
SUCCESS_STATUS,
|
||||
} from "@calcom/platform-constants";
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
CreatePrivateLinkInput,
|
||||
CreatePrivateLinkOutput,
|
||||
DeletePrivateLinkOutput,
|
||||
GetPrivateLinksOutput,
|
||||
UpdatePrivateLinkInput,
|
||||
UpdatePrivateLinkOutput,
|
||||
UpdatePrivateLinkBody,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
import { PrivateLinksService } from "../services/private-links.service";
|
||||
|
||||
class UpdatePrivateLinkBody extends OmitType(UpdatePrivateLinkInput, ["linkId"] as const) {}
|
||||
|
||||
@Controller({
|
||||
path: "/v2/event-types/:eventTypeId/private-links",
|
||||
})
|
||||
@@ -67,10 +51,9 @@ export class EventTypesPrivateLinksController {
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({ summary: "Get all private links for an event type" })
|
||||
async getPrivateLinks(
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@GetUser("id") userId: number
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
|
||||
): Promise<GetPrivateLinksOutput> {
|
||||
const privateLinks = await this.privateLinksService.getPrivateLinks(eventTypeId, userId);
|
||||
const privateLinks = await this.privateLinksService.getPrivateLinks(eventTypeId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -86,11 +69,10 @@ export class EventTypesPrivateLinksController {
|
||||
async updatePrivateLink(
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Param("linkId") linkId: string,
|
||||
@Body() body: UpdatePrivateLinkBody,
|
||||
@GetUser("id") userId: number
|
||||
@Body() body: UpdatePrivateLinkBody
|
||||
): Promise<UpdatePrivateLinkOutput> {
|
||||
const updateInput = { ...body, linkId };
|
||||
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, userId, updateInput);
|
||||
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, updateInput);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -105,10 +87,9 @@ export class EventTypesPrivateLinksController {
|
||||
@ApiOperation({ summary: "Delete a private link for an event type" })
|
||||
async deletePrivateLink(
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Param("linkId") linkId: string,
|
||||
@GetUser("id") userId: number
|
||||
@Param("linkId") linkId: string
|
||||
): Promise<DeletePrivateLinkOutput> {
|
||||
await this.privateLinksService.deletePrivateLink(eventTypeId, userId, linkId);
|
||||
await this.privateLinksService.deletePrivateLink(eventTypeId, linkId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -119,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",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { EventTypesPrivateLinksController } from "./controllers/event-types-private-links.controller";
|
||||
import { PrivateLinksRepository } from "./private-links.repository";
|
||||
import { PrivateLinksInputService } from "./services/private-links-input.service";
|
||||
import { PrivateLinksOutputService } from "./services/private-links-output.service";
|
||||
import { PrivateLinksService } from "./services/private-links.service";
|
||||
import { PrivateLinksRepository } from "./private-links.repository";
|
||||
|
||||
@Module({
|
||||
imports: [TokensModule, OAuthClientModule, PrismaModule, EventTypesModule_2024_06_14],
|
||||
imports: [TokensModule, PrismaModule, EventTypesModule_2024_06_14],
|
||||
controllers: [EventTypesPrivateLinksController],
|
||||
providers: [
|
||||
PrivateLinksService,
|
||||
@@ -21,7 +21,6 @@ import { PrivateLinksRepository } from "./private-links.repository";
|
||||
PrivateLinksRepository,
|
||||
EventTypeOwnershipGuard,
|
||||
],
|
||||
exports: [PrivateLinksService],
|
||||
})
|
||||
export class EventTypesPrivateLinksModule {}
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,10 @@ export class PrivateLinksRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async create(eventTypeId: number, link: { link: string; expiresAt: Date | null; maxUsageCount?: number | null }) {
|
||||
async create(
|
||||
eventTypeId: number,
|
||||
link: { link: string; expiresAt: Date | null; maxUsageCount?: number | null }
|
||||
) {
|
||||
return this.dbWrite.prisma.hashedLink.create({
|
||||
data: {
|
||||
eventTypeId,
|
||||
@@ -36,7 +39,10 @@ export class PrivateLinksRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async update(eventTypeId: number, link: { link: string; expiresAt: Date | null; maxUsageCount?: number | null }) {
|
||||
async update(
|
||||
eventTypeId: number,
|
||||
link: { link: string; expiresAt: Date | null; maxUsageCount?: number | null }
|
||||
) {
|
||||
return this.dbWrite.prisma.hashedLink.updateMany({
|
||||
where: { eventTypeId, link: link.link },
|
||||
data: {
|
||||
@@ -50,5 +56,3 @@ export class PrivateLinksRepository {
|
||||
return this.dbWrite.prisma.hashedLink.deleteMany({ where: { eventTypeId, link: linkId } });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+6
-3
@@ -1,6 +1,12 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import {
|
||||
PrivateLinkOutput,
|
||||
TimeBasedPrivateLinkOutput,
|
||||
UsageBasedPrivateLinkOutput,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
export type PrivateLinkData = {
|
||||
id: string;
|
||||
eventTypeId: number;
|
||||
@@ -10,7 +16,6 @@ export type PrivateLinkData = {
|
||||
maxUsageCount?: number | null;
|
||||
usageCount?: number;
|
||||
};
|
||||
import { PrivateLinkOutput, TimeBasedPrivateLinkOutput, UsageBasedPrivateLinkOutput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class PrivateLinksOutputService {
|
||||
@@ -39,5 +44,3 @@ export class PrivateLinksOutputService {
|
||||
return data.map((item) => this.transformToOutput(item));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
|
||||
import { generateHashedLink, isLinkExpired } from "@calcom/platform-libraries/private-links";
|
||||
import { CreatePrivateLinkInput, PrivateLinkOutput, UpdatePrivateLinkInput } from "@calcom/platform-types";
|
||||
|
||||
import { PrivateLinksRepository } from "@/ee/event-types-private-links/private-links.repository";
|
||||
import { PrivateLinksInputService } from "@/ee/event-types-private-links/services/private-links-input.service";
|
||||
import {
|
||||
PrivateLinksOutputService,
|
||||
type PrivateLinkData,
|
||||
} from "@/ee/event-types-private-links/services/private-links-output.service";
|
||||
import { PrivateLinksRepository } from "@/ee/event-types-private-links/private-links.repository";
|
||||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
|
||||
import { generateHashedLink, isLinkExpired } from "@calcom/platform-libraries/private-links";
|
||||
import { CreatePrivateLinkInput, PrivateLinkOutput, UpdatePrivateLinkInput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class PrivateLinksService {
|
||||
@@ -48,7 +47,7 @@ export class PrivateLinksService {
|
||||
}
|
||||
}
|
||||
|
||||
async getPrivateLinks(eventTypeId: number, userId: number): Promise<PrivateLinkOutput[]> {
|
||||
async getPrivateLinks(eventTypeId: number): Promise<PrivateLinkOutput[]> {
|
||||
try {
|
||||
const links = await this.repo.listByEventTypeId(eventTypeId);
|
||||
const mapped: PrivateLinkData[] = links.map((l) => ({
|
||||
@@ -69,11 +68,7 @@ export class PrivateLinksService {
|
||||
}
|
||||
}
|
||||
|
||||
async updatePrivateLink(
|
||||
eventTypeId: number,
|
||||
userId: number,
|
||||
input: UpdatePrivateLinkInput
|
||||
): Promise<PrivateLinkOutput> {
|
||||
async updatePrivateLink(eventTypeId: number, input: UpdatePrivateLinkInput): Promise<PrivateLinkOutput> {
|
||||
try {
|
||||
const transformedInput = this.inputService.transformUpdateInput(input);
|
||||
const updatedResult = await this.repo.update(eventTypeId, {
|
||||
@@ -107,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 {
|
||||
const { count } = await this.repo.delete(eventTypeId, linkId);
|
||||
if (count === 0) {
|
||||
@@ -124,5 +119,3 @@ export class PrivateLinksService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ import { UpdateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2
|
||||
import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer";
|
||||
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
||||
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
||||
|
||||
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
@@ -42,7 +41,6 @@ import {
|
||||
GetEventTypesQuery_2024_06_14,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BookingsModule_2024_04_15 } from "@/ee/bookings/2024-04-15/bookings.module";
|
||||
import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module";
|
||||
import { CalendarsModule } from "@/ee/calendars/calendars.module";
|
||||
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { GcalModule } from "@/ee/gcal/gcal.module";
|
||||
@@ -14,7 +15,6 @@ import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.m
|
||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||
import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module";
|
||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
|
||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
|
||||
+52
-2
@@ -2,10 +2,21 @@ import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { GetUnifiedCalendarEventOutput } from "@/modules/cal-unified-calendars/outputs/get-unified-calendar-event";
|
||||
import { UpdateUnifiedCalendarEventInput } from "@/modules/cal-unified-calendars/inputs/update-unified-calendar-event.input";
|
||||
import { GetUnifiedCalendarEventOutput } from "@/modules/cal-unified-calendars/outputs/get-unified-calendar-event.output";
|
||||
import { GoogleCalendarEventOutputPipe } from "@/modules/cal-unified-calendars/pipes/get-calendar-event-details-output-pipe";
|
||||
import { GoogleCalendarService } from "@/modules/cal-unified-calendars/services/google-calendar.service";
|
||||
import { Controller, Get, Param, UseGuards, HttpCode, HttpStatus, BadRequestException } from "@nestjs/common";
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
UseGuards,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
BadRequestException,
|
||||
Patch,
|
||||
Body,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags, ApiParam, ApiHeader, ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { GOOGLE_CALENDAR, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
@@ -30,6 +41,7 @@ export class CalUnifiedCalendarsController {
|
||||
type: String,
|
||||
})
|
||||
@Get("/:calendar/event/:eventUid")
|
||||
@Get("/:calendar/events/:eventUid")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@@ -54,4 +66,42 @@ export class CalUnifiedCalendarsController {
|
||||
data: transformedEvent,
|
||||
};
|
||||
}
|
||||
|
||||
@ApiParam({
|
||||
name: "calendar",
|
||||
enum: [GOOGLE_CALENDAR],
|
||||
type: String,
|
||||
})
|
||||
@ApiParam({
|
||||
name: "eventUid",
|
||||
description:
|
||||
"The Google Calendar event ID. You can retrieve this by getting booking references from the following endpoints:\n\n- For team events: https://cal.com/docs/api-reference/v2/orgs-teams-bookings/get-booking-references-for-a-booking\n\n- For user events: https://cal.com/docs/api-reference/v2/bookings/get-booking-references-for-a-booking",
|
||||
type: String,
|
||||
})
|
||||
@Patch("/:calendar/events/:eventUid")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Update meeting details in calendar",
|
||||
description: "Updates event information in the specified calendar provider",
|
||||
})
|
||||
async updateCalendarEvent(
|
||||
@Param("calendar") calendar: string,
|
||||
@Param("eventUid") eventUid: string,
|
||||
@Body() updateData: UpdateUnifiedCalendarEventInput
|
||||
): Promise<GetUnifiedCalendarEventOutput> {
|
||||
if (calendar !== GOOGLE_CALENDAR) {
|
||||
throw new BadRequestException("Event updates are currently only available for Google Calendar");
|
||||
}
|
||||
|
||||
const updatedEvent = await this.googleCalendarService.updateEventDetails(eventUid, updateData);
|
||||
|
||||
const transformedEvent = new GoogleCalendarEventOutputPipe().transform(updatedEvent);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: transformedEvent,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsISO8601, IsOptional, IsString, ValidateNested, IsEnum, IsArray } from "class-validator";
|
||||
|
||||
import { CalendarEventStatus, CalendarEventResponseStatus } from "../outputs/get-unified-calendar-event.output";
|
||||
|
||||
export class UpdateCalendarEventAttendee {
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "Email address of the attendee",
|
||||
})
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "Display name of the attendee",
|
||||
})
|
||||
name?: string;
|
||||
|
||||
@IsEnum(CalendarEventResponseStatus)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
enum: CalendarEventResponseStatus,
|
||||
enumName: "CalendarEventResponseStatus",
|
||||
nullable: true,
|
||||
description: "Response status of the attendee",
|
||||
})
|
||||
responseStatus?: CalendarEventResponseStatus | null;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Indicates if this attendee is the current user",
|
||||
})
|
||||
self?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Indicates if this attendee's attendance is optional",
|
||||
})
|
||||
optional?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Indicates if this attendee is the host",
|
||||
})
|
||||
host?: boolean;
|
||||
|
||||
}
|
||||
|
||||
export class UpdateDateTimeWithZone {
|
||||
@IsISO8601()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: "string", format: "date-time" })
|
||||
time?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
timeZone?: string;
|
||||
}
|
||||
|
||||
export class UpdateUnifiedCalendarEventInput {
|
||||
@ValidateNested()
|
||||
@Type(() => UpdateDateTimeWithZone)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: "object",
|
||||
properties: {
|
||||
time: { type: "string", format: "date-time" },
|
||||
timeZone: { type: "string" },
|
||||
},
|
||||
description: "Start date and time of the calendar event with timezone information",
|
||||
})
|
||||
start?: UpdateDateTimeWithZone;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => UpdateDateTimeWithZone)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: "object",
|
||||
properties: {
|
||||
time: { type: "string", format: "date-time" },
|
||||
timeZone: { type: "string" },
|
||||
},
|
||||
description: "End date and time of the calendar event with timezone information",
|
||||
})
|
||||
end?: UpdateDateTimeWithZone;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "Title of the calendar event",
|
||||
})
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
nullable: true,
|
||||
description: "Detailed description of the calendar event",
|
||||
})
|
||||
description?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => UpdateCalendarEventAttendee)
|
||||
@ApiPropertyOptional({
|
||||
type: [UpdateCalendarEventAttendee],
|
||||
nullable: true,
|
||||
description:
|
||||
"List of attendees. CAUTION: You must pass the entire array with all updated values. Any attendees not included in this array will be removed from the event.",
|
||||
})
|
||||
attendees?: UpdateCalendarEventAttendee[];
|
||||
|
||||
@IsEnum(CalendarEventStatus)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
enum: CalendarEventStatus,
|
||||
enumName: "CalendarEventStatus",
|
||||
nullable: true,
|
||||
description: "Status of the event (accepted, pending, declined, cancelled)",
|
||||
example: CalendarEventStatus.ACCEPTED,
|
||||
})
|
||||
status?: CalendarEventStatus | null;
|
||||
}
|
||||
+34
@@ -225,6 +225,22 @@ export class CalendarEventHost {
|
||||
responseStatus!: CalendarEventResponseStatus | null;
|
||||
}
|
||||
|
||||
export class calendarEventOwner {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: "Email address of the event host",
|
||||
})
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Display name of the event host",
|
||||
})
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export class CalendarEventAttendee {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
@@ -264,6 +280,14 @@ export class CalendarEventAttendee {
|
||||
description: "Indicates if this attendee's attendance is optional",
|
||||
})
|
||||
optional?: boolean;
|
||||
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Indicates if this attendee is the host",
|
||||
})
|
||||
host?: boolean;
|
||||
}
|
||||
|
||||
export class DateTimeWithZone {
|
||||
@@ -384,6 +408,16 @@ export class UnifiedCalendarEventOutput {
|
||||
})
|
||||
hosts?: CalendarEventHost[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => calendarEventOwner)
|
||||
@ApiPropertyOptional({
|
||||
type: calendarEventOwner,
|
||||
nullable: true,
|
||||
description: "The calendar account that owns this event. This is the primary calendar where the event is stored and cannot be modified without appropriate permissions. Changing this would require moving the event to a different calendar",
|
||||
})
|
||||
calendarEventOwner?: calendarEventOwner;
|
||||
|
||||
@IsEnum(CALENDARS)
|
||||
@ApiProperty({
|
||||
enum: CALENDARS,
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { GoogleCalendarEventResponse } from "../get-calendar-event-details-output-pipe";
|
||||
|
||||
export const createGoogleCalendarEventFixture = (
|
||||
overrides: Partial<GoogleCalendarEventResponse> = {}
|
||||
): GoogleCalendarEventResponse => {
|
||||
const baseEvent: GoogleCalendarEventResponse = {
|
||||
kind: "calendar#event",
|
||||
etag: "test-etag",
|
||||
id: "test-event-id",
|
||||
status: "confirmed",
|
||||
htmlLink: "https://calendar.google.com/event",
|
||||
created: "2024-01-01T00:00:00Z",
|
||||
updated: "2024-01-01T00:00:00Z",
|
||||
summary: "Test Meeting",
|
||||
description: "Test description",
|
||||
creator: {
|
||||
email: "creator@example.com",
|
||||
displayName: "Creator Name",
|
||||
},
|
||||
organizer: {
|
||||
email: "organizer@example.com",
|
||||
displayName: "Organizer Name",
|
||||
},
|
||||
start: {
|
||||
dateTime: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
end: {
|
||||
dateTime: "2024-01-15T11:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
iCalUID: "test-ical-uid",
|
||||
sequence: 0,
|
||||
attendees: [
|
||||
{
|
||||
email: "attendee@example.com",
|
||||
displayName: "Attendee Name",
|
||||
responseStatus: "accepted",
|
||||
organizer: false,
|
||||
},
|
||||
{
|
||||
email: "organizer@example.com",
|
||||
displayName: "Organizer Name",
|
||||
responseStatus: "accepted",
|
||||
organizer: true,
|
||||
},
|
||||
],
|
||||
conferenceData: {
|
||||
conferenceId: "abc-def-ghi",
|
||||
entryPoints: [
|
||||
{
|
||||
entryPointType: "video",
|
||||
uri: "https://meet.google.com/abc-def-ghi",
|
||||
label: "meet.google.com/abc-def-ghi",
|
||||
},
|
||||
],
|
||||
conferenceSolution: {
|
||||
key: {
|
||||
type: "hangoutsMeet",
|
||||
},
|
||||
name: "Google Meet",
|
||||
iconUri:
|
||||
"https://fonts.gstatic.com/s/i/productlogos/meet_2020q4/v6/web-512dp/logo_meet_2020q4_color_2x_web_512dp.png",
|
||||
},
|
||||
},
|
||||
hangoutLink: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
return { ...baseEvent, ...overrides };
|
||||
};
|
||||
|
||||
export const googleEventWithConferenceData = createGoogleCalendarEventFixture({
|
||||
conferenceData: {
|
||||
conferenceId: "test-conference-id",
|
||||
entryPoints: [
|
||||
{
|
||||
entryPointType: "video",
|
||||
uri: "https://meet.google.com/abc-def-ghi",
|
||||
label: "Google Meet",
|
||||
pin: "123456",
|
||||
regionCode: "US",
|
||||
},
|
||||
{
|
||||
entryPointType: "phone",
|
||||
uri: "tel:+1-555-123-4567",
|
||||
label: "Phone",
|
||||
pin: "789012",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const googleEventWithLocationOnly = createGoogleCalendarEventFixture({
|
||||
location: "123 Main St, City, State",
|
||||
conferenceData: undefined,
|
||||
hangoutLink: undefined,
|
||||
});
|
||||
|
||||
export const googleEventWithHangoutLink = createGoogleCalendarEventFixture({
|
||||
hangoutLink: "https://hangouts.google.com/call/abc123",
|
||||
conferenceData: undefined,
|
||||
location: undefined,
|
||||
});
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
import {
|
||||
CalendarEventStatus,
|
||||
CalendarEventResponseStatus,
|
||||
} from "@/modules/cal-unified-calendars/outputs/get-unified-calendar-event.output";
|
||||
|
||||
import {
|
||||
createGoogleCalendarEventFixture,
|
||||
googleEventWithConferenceData,
|
||||
googleEventWithLocationOnly,
|
||||
googleEventWithHangoutLink,
|
||||
} from "./__fixtures__/google-calendar-event.fixture";
|
||||
import {
|
||||
GoogleCalendarEventOutputPipe,
|
||||
GoogleCalendarEventResponse,
|
||||
} from "./get-calendar-event-details-output-pipe";
|
||||
|
||||
describe("GoogleCalendarEventOutputPipe", () => {
|
||||
let pipe: GoogleCalendarEventOutputPipe;
|
||||
let sharedGoogleEvent: GoogleCalendarEventResponse;
|
||||
|
||||
beforeAll(() => {
|
||||
sharedGoogleEvent = createGoogleCalendarEventFixture();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
pipe = new GoogleCalendarEventOutputPipe();
|
||||
});
|
||||
|
||||
describe("transform", () => {
|
||||
it("should transform Google Calendar event to unified format", () => {
|
||||
const result = pipe.transform(sharedGoogleEvent);
|
||||
|
||||
expect(result.id).toBe("test-event-id");
|
||||
expect(result.title).toBe("Test Meeting");
|
||||
expect(result.description).toBe("Test description");
|
||||
expect(result.source).toBe("google");
|
||||
expect(result.status).toBe(CalendarEventStatus.ACCEPTED);
|
||||
expect(result.start.time).toBe("2024-01-15T10:00:00Z");
|
||||
expect(result.start.timeZone).toBe("America/New_York");
|
||||
expect(result.end.time).toBe("2024-01-15T11:00:00Z");
|
||||
expect(result.end.timeZone).toBe("America/New_York");
|
||||
expect(result.attendees).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformDateTimeWithZone", () => {
|
||||
it("should transform Google date time to unified format", () => {
|
||||
const googleDateTime = {
|
||||
dateTime: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
};
|
||||
|
||||
const result = pipe["transformDateTimeWithZone"](googleDateTime);
|
||||
|
||||
expect(result.time).toBe("2024-01-15T10:00:00Z");
|
||||
expect(result.timeZone).toBe("America/New_York");
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformAttendeeResponseStatus", () => {
|
||||
it("should transform accepted status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"]("accepted");
|
||||
expect(result).toBe(CalendarEventResponseStatus.ACCEPTED);
|
||||
});
|
||||
|
||||
it("should transform tentative status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"]("tentative");
|
||||
expect(result).toBe(CalendarEventResponseStatus.PENDING);
|
||||
});
|
||||
|
||||
it("should transform declined status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"]("declined");
|
||||
expect(result).toBe(CalendarEventResponseStatus.DECLINED);
|
||||
});
|
||||
|
||||
it("should transform needsaction status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"]("needsaction");
|
||||
expect(result).toBe(CalendarEventResponseStatus.NEEDS_ACTION);
|
||||
});
|
||||
|
||||
it("should handle case insensitive status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"]("ACCEPTED");
|
||||
expect(result).toBe(CalendarEventResponseStatus.ACCEPTED);
|
||||
});
|
||||
|
||||
it("should handle unknown status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"]("unknown");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle null status", () => {
|
||||
const result = pipe["transformAttendeeResponseStatus"](undefined);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformEventStatus", () => {
|
||||
it("should transform confirmed status", () => {
|
||||
const result = pipe["transformEventStatus"]("confirmed");
|
||||
expect(result).toBe(CalendarEventStatus.ACCEPTED);
|
||||
});
|
||||
|
||||
it("should transform tentative status", () => {
|
||||
const result = pipe["transformEventStatus"]("tentative");
|
||||
expect(result).toBe(CalendarEventStatus.PENDING);
|
||||
});
|
||||
|
||||
it("should transform cancelled status", () => {
|
||||
const result = pipe["transformEventStatus"]("cancelled");
|
||||
expect(result).toBe(CalendarEventStatus.CANCELLED);
|
||||
});
|
||||
|
||||
it("should handle case insensitive status", () => {
|
||||
const result = pipe["transformEventStatus"]("CONFIRMED");
|
||||
expect(result).toBe(CalendarEventStatus.ACCEPTED);
|
||||
});
|
||||
|
||||
it("should handle unknown status", () => {
|
||||
const result = pipe["transformEventStatus"]("unknown");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle null status", () => {
|
||||
const result = pipe["transformEventStatus"](undefined);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformOrganizer", () => {
|
||||
it("should return calendarEventOwner organizer exist", () => {
|
||||
const eventWithoutOrganizerAttendees = createGoogleCalendarEventFixture({
|
||||
organizer: { email: "organizer@example.com" },
|
||||
attendees: [
|
||||
{
|
||||
email: "attendee@example.com",
|
||||
displayName: "Regular Attendee",
|
||||
responseStatus: "accepted",
|
||||
organizer: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = pipe.transform(eventWithoutOrganizerAttendees);
|
||||
|
||||
expect(result?.calendarEventOwner?.email).toBe("organizer@example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformLocations", () => {
|
||||
it("should transform conference data entry points", () => {
|
||||
const result = pipe["transformLocations"](googleEventWithConferenceData);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
type: "video",
|
||||
url: "https://meet.google.com/abc-def-ghi",
|
||||
label: "Google Meet",
|
||||
pin: "123456",
|
||||
regionCode: "US",
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
type: "phone",
|
||||
url: "tel:+1-555-123-4567",
|
||||
label: "Phone",
|
||||
pin: "789012",
|
||||
regionCode: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fallback to location field", () => {
|
||||
const result = pipe["transformLocations"](googleEventWithLocationOnly);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
type: "video",
|
||||
url: "123 Main St, City, State",
|
||||
});
|
||||
});
|
||||
|
||||
it("should fallback to hangout link", () => {
|
||||
const result = pipe["transformLocations"](googleEventWithHangoutLink);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
type: "video",
|
||||
url: "https://hangouts.google.com/call/abc123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty array when no location data", () => {
|
||||
const eventWithoutLocation = createGoogleCalendarEventFixture({
|
||||
location: undefined,
|
||||
conferenceData: undefined,
|
||||
hangoutLink: undefined,
|
||||
});
|
||||
|
||||
const result = pipe["transformLocations"](eventWithoutLocation);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+7
-6
@@ -3,7 +3,7 @@ import {
|
||||
CalendarEventStatus,
|
||||
DateTimeWithZone,
|
||||
UnifiedCalendarEventOutput,
|
||||
} from "@/modules/cal-unified-calendars/outputs/get-unified-calendar-event";
|
||||
} from "@/modules/cal-unified-calendars/outputs/get-unified-calendar-event.output";
|
||||
import { PipeTransform, Injectable } from "@nestjs/common";
|
||||
|
||||
export interface GoogleCalendarEventResponse {
|
||||
@@ -94,14 +94,12 @@ export class GoogleCalendarEventOutputPipe
|
||||
calendarEvent.locations = this.transformLocations(googleEvent);
|
||||
|
||||
if (googleEvent.attendees && googleEvent.attendees.length > 0) {
|
||||
calendarEvent.attendees = googleEvent.attendees
|
||||
.filter((attendee) => !attendee.organizer)
|
||||
.map((attendee) => {
|
||||
calendarEvent.attendees = googleEvent.attendees.map((attendee) => {
|
||||
return {
|
||||
email: attendee.email,
|
||||
name: attendee.displayName,
|
||||
responseStatus: this.transformAttendeeResponseStatus(attendee.responseStatus),
|
||||
organizer: attendee.organizer,
|
||||
host: attendee.organizer,
|
||||
self: attendee.self,
|
||||
optional: attendee.optional,
|
||||
};
|
||||
@@ -110,7 +108,8 @@ export class GoogleCalendarEventOutputPipe
|
||||
|
||||
calendarEvent.status = this.transformEventStatus(googleEvent.status);
|
||||
|
||||
calendarEvent.hosts = this.transformHosts(googleEvent);
|
||||
calendarEvent.calendarEventOwner = googleEvent.organizer
|
||||
calendarEvent.hosts = this.transformHosts(googleEvent)
|
||||
|
||||
calendarEvent.source = "google";
|
||||
|
||||
@@ -196,6 +195,8 @@ export class GoogleCalendarEventOutputPipe
|
||||
regionCode: entryPoint.regionCode,
|
||||
};
|
||||
});
|
||||
} else if (googleEvent.location) {
|
||||
return [{ type: "video", url: googleEvent.location }];
|
||||
} else if (googleEvent.hangoutLink) {
|
||||
return [{ type: "video", url: googleEvent.hangoutLink }];
|
||||
}
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { GoogleCalendarEventResponse } from "@/modules/cal-unified-calendars/pipes/get-calendar-event-details-output-pipe";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
UpdateUnifiedCalendarEventInput,
|
||||
UpdateDateTimeWithZone,
|
||||
} from "../inputs/update-unified-calendar-event.input";
|
||||
import { CalendarEventResponseStatus, CalendarEventStatus } from "../outputs/get-unified-calendar-event.output";
|
||||
|
||||
// Common interfaces for Google Calendar types
|
||||
interface GoogleCalendarDateTime {
|
||||
dateTime: string;
|
||||
timeZone: string;
|
||||
}
|
||||
|
||||
interface GoogleCalendarAttendee {
|
||||
email: string;
|
||||
displayName?: string;
|
||||
responseStatus: string;
|
||||
organizer?: boolean;
|
||||
}
|
||||
|
||||
interface GoogleCalendarEventInputTransform {
|
||||
transform(
|
||||
updateData: UpdateUnifiedCalendarEventInput,
|
||||
fetchedEvent?: GoogleCalendarEventResponse | null
|
||||
): any;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GoogleCalendarEventInputPipe implements GoogleCalendarEventInputTransform {
|
||||
transform(
|
||||
updateData: UpdateUnifiedCalendarEventInput,
|
||||
): any {
|
||||
const updatePayload: any = {};
|
||||
|
||||
if (updateData.title !== undefined) {
|
||||
updatePayload.summary = updateData.title;
|
||||
}
|
||||
|
||||
if (updateData.description !== undefined) {
|
||||
updatePayload.description = updateData.description;
|
||||
}
|
||||
|
||||
if (updateData.start) {
|
||||
updatePayload.start = this.transformDateTimeWithZone(updateData.start);
|
||||
}
|
||||
|
||||
if (updateData.end) {
|
||||
updatePayload.end = this.transformDateTimeWithZone(updateData.end);
|
||||
}
|
||||
|
||||
if (updateData.attendees !== undefined) {
|
||||
updatePayload.attendees = this.transformAttendees(
|
||||
updateData.attendees,
|
||||
);
|
||||
}
|
||||
|
||||
if (updateData.status !== undefined) {
|
||||
updatePayload.status = this.transformEventStatus(updateData.status);
|
||||
}
|
||||
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
private transformDateTimeWithZone(dateTime: UpdateDateTimeWithZone): GoogleCalendarDateTime {
|
||||
return {
|
||||
dateTime: dateTime.time || "",
|
||||
timeZone: dateTime.timeZone || "",
|
||||
};
|
||||
}
|
||||
|
||||
private transformAttendees(
|
||||
inputAttendees: NonNullable<UpdateUnifiedCalendarEventInput["attendees"]>,
|
||||
): GoogleCalendarAttendee[] {
|
||||
return inputAttendees.map((attendee) => {
|
||||
return {
|
||||
email: attendee.email,
|
||||
displayName: attendee.name,
|
||||
responseStatus: this.transformResponseStatus(attendee.responseStatus),
|
||||
organizer: attendee.host,
|
||||
self: attendee.self,
|
||||
optional: attendee.optional,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private transformResponseStatus(responseStatus?: CalendarEventResponseStatus | null): string {
|
||||
if (!responseStatus) return "needsAction";
|
||||
|
||||
switch (responseStatus) {
|
||||
case CalendarEventResponseStatus.ACCEPTED:
|
||||
return "accepted";
|
||||
case CalendarEventResponseStatus.PENDING:
|
||||
return "tentative";
|
||||
case CalendarEventResponseStatus.DECLINED:
|
||||
return "declined";
|
||||
case CalendarEventResponseStatus.NEEDS_ACTION:
|
||||
return "needsAction";
|
||||
default:
|
||||
return "needsAction";
|
||||
}
|
||||
}
|
||||
|
||||
private transformEventStatus(status?: CalendarEventStatus | null): string {
|
||||
if (!status) return "confirmed";
|
||||
|
||||
switch (status) {
|
||||
case CalendarEventStatus.ACCEPTED:
|
||||
return "confirmed";
|
||||
case CalendarEventStatus.PENDING:
|
||||
return "tentative";
|
||||
case CalendarEventStatus.CANCELLED:
|
||||
return "cancelled";
|
||||
case CalendarEventStatus.DECLINED:
|
||||
return "cancelled";
|
||||
default:
|
||||
return "confirmed";
|
||||
}
|
||||
}
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
import {
|
||||
UpdateUnifiedCalendarEventInput,
|
||||
UpdateDateTimeWithZone,
|
||||
} from "../inputs/update-unified-calendar-event.input";
|
||||
import { CalendarEventResponseStatus, CalendarEventStatus } from "../outputs/get-unified-calendar-event.output";
|
||||
import { GoogleCalendarEventResponse } from "./get-calendar-event-details-output-pipe";
|
||||
import { GoogleCalendarEventInputPipe } from "./google-calendar-event-input-pipe";
|
||||
|
||||
describe("GoogleCalendarEventInputPipe", () => {
|
||||
let pipe: GoogleCalendarEventInputPipe;
|
||||
let sharedGoogleEvent: GoogleCalendarEventResponse;
|
||||
|
||||
beforeAll(() => {
|
||||
sharedGoogleEvent = {
|
||||
kind: "calendar#event",
|
||||
etag: "test-etag",
|
||||
id: "test-event-id",
|
||||
status: "confirmed",
|
||||
htmlLink: "https://calendar.google.com/event",
|
||||
created: "2024-01-01T00:00:00Z",
|
||||
updated: "2024-01-01T00:00:00Z",
|
||||
summary: "Test Meeting",
|
||||
description: "Test description",
|
||||
start: {
|
||||
dateTime: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
end: {
|
||||
dateTime: "2024-01-15T11:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
iCalUID: "test-ical-uid",
|
||||
sequence: 0,
|
||||
attendees: [
|
||||
{
|
||||
email: "attendee1@example.com",
|
||||
displayName: "Attendee One",
|
||||
responseStatus: "accepted",
|
||||
organizer: false,
|
||||
},
|
||||
{
|
||||
email: "attendee2@example.com",
|
||||
displayName: "Attendee Two",
|
||||
responseStatus: "needsAction",
|
||||
organizer: false,
|
||||
},
|
||||
{
|
||||
email: "organizer@example.com",
|
||||
displayName: "Organizer",
|
||||
responseStatus: "accepted",
|
||||
organizer: true,
|
||||
},
|
||||
],
|
||||
creator: {
|
||||
email: "organizer@example.com",
|
||||
displayName: "Organizer",
|
||||
},
|
||||
organizer: {
|
||||
email: "organizer@example.com",
|
||||
displayName: "Organizer",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
pipe = new GoogleCalendarEventInputPipe();
|
||||
});
|
||||
|
||||
describe("transform", () => {
|
||||
it("should transform basic event fields", () => {
|
||||
const input: UpdateUnifiedCalendarEventInput = {
|
||||
title: "Updated Meeting",
|
||||
description: "Updated description",
|
||||
start: {
|
||||
time: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
end: {
|
||||
time: "2024-01-15T11:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
status: CalendarEventStatus.ACCEPTED,
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
summary: "Updated Meeting",
|
||||
description: "Updated description",
|
||||
start: {
|
||||
dateTime: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
end: {
|
||||
dateTime: "2024-01-15T11:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
status: "confirmed",
|
||||
};
|
||||
|
||||
const result = pipe.transform(input);
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should handle partial updates", () => {
|
||||
const input: UpdateUnifiedCalendarEventInput = {
|
||||
title: "Only Title Update",
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
summary: "Only Title Update",
|
||||
};
|
||||
|
||||
const result = pipe.transform(input);
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should handle null description", () => {
|
||||
const input: UpdateUnifiedCalendarEventInput = {
|
||||
description: null,
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
description: null,
|
||||
};
|
||||
|
||||
const result = pipe.transform(input);
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformDateTimeWithZone", () => {
|
||||
it("should transform date time with timezone", () => {
|
||||
const input: UpdateDateTimeWithZone = {
|
||||
time: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
dateTime: "2024-01-15T10:00:00Z",
|
||||
timeZone: "America/New_York",
|
||||
};
|
||||
|
||||
const result = pipe["transformDateTimeWithZone"](input);
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should handle empty time and timezone", () => {
|
||||
const input: UpdateDateTimeWithZone = {};
|
||||
|
||||
const expectedOutput = {
|
||||
dateTime: "",
|
||||
timeZone: "",
|
||||
};
|
||||
|
||||
const result = pipe["transformDateTimeWithZone"](input);
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformResponseStatus", () => {
|
||||
it("should transform ACCEPTED status", () => {
|
||||
const result = pipe["transformResponseStatus"](CalendarEventResponseStatus.ACCEPTED);
|
||||
expect(result).toBe("accepted");
|
||||
});
|
||||
|
||||
it("should transform PENDING status", () => {
|
||||
const result = pipe["transformResponseStatus"](CalendarEventResponseStatus.PENDING);
|
||||
expect(result).toBe("tentative");
|
||||
});
|
||||
|
||||
it("should transform DECLINED status", () => {
|
||||
const result = pipe["transformResponseStatus"](CalendarEventResponseStatus.DECLINED);
|
||||
expect(result).toBe("declined");
|
||||
});
|
||||
|
||||
it("should transform NEEDS_ACTION status", () => {
|
||||
const result = pipe["transformResponseStatus"](CalendarEventResponseStatus.NEEDS_ACTION);
|
||||
expect(result).toBe("needsAction");
|
||||
});
|
||||
|
||||
it("should handle null status", () => {
|
||||
const result = pipe["transformResponseStatus"](null);
|
||||
expect(result).toBe("needsAction");
|
||||
});
|
||||
|
||||
it("should handle undefined status", () => {
|
||||
const result = pipe["transformResponseStatus"](undefined);
|
||||
expect(result).toBe("needsAction");
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformEventStatus", () => {
|
||||
it("should transform ACCEPTED status", () => {
|
||||
const result = pipe["transformEventStatus"](CalendarEventStatus.ACCEPTED);
|
||||
expect(result).toBe("confirmed");
|
||||
});
|
||||
|
||||
it("should transform PENDING status", () => {
|
||||
const result = pipe["transformEventStatus"](CalendarEventStatus.PENDING);
|
||||
expect(result).toBe("tentative");
|
||||
});
|
||||
|
||||
it("should transform CANCELLED status", () => {
|
||||
const result = pipe["transformEventStatus"](CalendarEventStatus.CANCELLED);
|
||||
expect(result).toBe("cancelled");
|
||||
});
|
||||
|
||||
it("should transform DECLINED status", () => {
|
||||
const result = pipe["transformEventStatus"](CalendarEventStatus.DECLINED);
|
||||
expect(result).toBe("cancelled");
|
||||
});
|
||||
|
||||
it("should handle null status", () => {
|
||||
const result = pipe["transformEventStatus"](null);
|
||||
expect(result).toBe("confirmed");
|
||||
});
|
||||
|
||||
it("should handle undefined status", () => {
|
||||
const result = pipe["transformEventStatus"](undefined);
|
||||
expect(result).toBe("confirmed");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,8 @@ import { JWT } from "googleapis-common";
|
||||
|
||||
import { DelegationCredentialRepository, OAuth2UniversalSchema } from "@calcom/platform-libraries/app-store";
|
||||
|
||||
import { UnifiedCalendarEventOutput } from "../outputs/get-unified-calendar-event";
|
||||
import { UpdateUnifiedCalendarEventInput } from "../inputs/update-unified-calendar-event.input";
|
||||
import { GoogleCalendarEventInputPipe } from "../pipes/google-calendar-event-input-pipe";
|
||||
|
||||
@Injectable()
|
||||
export class GoogleCalendarService {
|
||||
@@ -53,6 +54,44 @@ export class GoogleCalendarService {
|
||||
}
|
||||
}
|
||||
|
||||
async updateEventDetails(
|
||||
eventUid: string,
|
||||
updateData: UpdateUnifiedCalendarEventInput
|
||||
): Promise<GoogleCalendarEventResponse> {
|
||||
const bookingReference =
|
||||
await this.bookingReferencesRepository.getBookingReferencesIncludeSensitiveCredentials(eventUid);
|
||||
|
||||
if (!bookingReference) {
|
||||
throw new NotFoundException("Booking reference not found");
|
||||
}
|
||||
|
||||
const ownerUserEmail = bookingReference?.booking?.user?.email;
|
||||
|
||||
const calendar = await this.getAuthorizedCalendarInstance(
|
||||
ownerUserEmail,
|
||||
bookingReference.credential?.key,
|
||||
bookingReference.delegationCredential
|
||||
);
|
||||
|
||||
|
||||
const updatePayload = new GoogleCalendarEventInputPipe().transform(updateData);
|
||||
|
||||
try {
|
||||
const event = await calendar.events.patch({
|
||||
calendarId: bookingReference?.externalCalendarId ?? "primary",
|
||||
eventId: bookingReference?.uid,
|
||||
requestBody: updatePayload,
|
||||
});
|
||||
|
||||
if (!event.data) {
|
||||
throw new NotFoundException("Failed to update meeting");
|
||||
}
|
||||
return event.data as GoogleCalendarEventResponse;
|
||||
} catch (error) {
|
||||
throw new NotFoundException("Failed to update meeting details");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an authorized Google Calendar instance
|
||||
* Tries delegation credentials first, falls back to direct OAuth
|
||||
|
||||
@@ -40,5 +40,3 @@ export class EventTypeOwnershipGuard implements CanActivate {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+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();
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,8 @@ import { OrganizationsConferencingModule } from "@/modules/organizations/confere
|
||||
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
||||
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
|
||||
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 { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
|
||||
import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service";
|
||||
@@ -91,6 +93,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsStripeModule,
|
||||
OrganizationsTeamsRoutingFormsModule,
|
||||
OrganizationsConferencingModule,
|
||||
EventTypesPrivateLinksModule,
|
||||
],
|
||||
providers: [
|
||||
OrganizationsRepository,
|
||||
@@ -177,6 +180,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsTeamsSchedulesController,
|
||||
OrganizationsUsersOOOController,
|
||||
OrganizationTeamWorkflowsController,
|
||||
OrganizationsEventTypesPrivateLinksController,
|
||||
],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
|
||||
@@ -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": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
|
||||
@@ -8563,6 +8849,69 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/calendars/{calendar}/events/{eventUid}": {
|
||||
"patch": {
|
||||
"operationId": "CalUnifiedCalendarsController_updateCalendarEvent",
|
||||
"summary": "Update meeting details in calendar",
|
||||
"description": "Updates event information in the specified calendar provider",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "calendar",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"google"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "eventUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "The Google Calendar event ID. You can retrieve this by getting booking references from the following endpoints:\n\n- For team events: https://cal.com/docs/api-reference/v2/orgs-teams-bookings/get-booking-references-for-a-booking\n\n- For user events: https://cal.com/docs/api-reference/v2/bookings/get-booking-references-for-a-booking",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateUnifiedCalendarEventInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetUnifiedCalendarEventOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Cal Unified Calendars"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/calendars/ics-feed/save": {
|
||||
"post": {
|
||||
"operationId": "CalendarsController_createIcsFeed",
|
||||
@@ -23726,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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -25574,6 +26130,23 @@
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -28219,230 +28792,6 @@
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -28782,7 +29131,7 @@
|
||||
},
|
||||
"CalendarEventResponseStatus": {
|
||||
"type": "string",
|
||||
"description": "Host's response to the invitation",
|
||||
"description": "Response status of the attendee",
|
||||
"enum": [
|
||||
"accepted",
|
||||
"pending",
|
||||
@@ -28815,6 +29164,11 @@
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "Indicates if this attendee's attendance is optional"
|
||||
},
|
||||
"host": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "Indicates if this attendee is the host"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -28853,6 +29207,23 @@
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"calendarEventOwner": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "Email address of the event host"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Display name of the event host"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"CalendarSource": {
|
||||
"type": "string",
|
||||
"description": "Calendar integration source (e.g., Google Calendar, Office 365, Apple Calendar). Currently only Google Calendar is supported.",
|
||||
@@ -28949,6 +29320,15 @@
|
||||
"$ref": "#/components/schemas/CalendarEventHost"
|
||||
}
|
||||
},
|
||||
"calendarEventOwner": {
|
||||
"nullable": true,
|
||||
"description": "The calendar account that owns this event. This is the primary calendar where the event is stored and cannot be modified without appropriate permissions. Changing this would require moving the event to a different calendar",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/calendarEventOwner"
|
||||
}
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"example": "google",
|
||||
"$ref": "#/components/schemas/CalendarSource"
|
||||
@@ -28982,6 +29362,91 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateCalendarEventAttendee": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "Email address of the attendee"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the attendee"
|
||||
},
|
||||
"responseStatus": {
|
||||
"nullable": true,
|
||||
"$ref": "#/components/schemas/CalendarEventResponseStatus"
|
||||
},
|
||||
"self": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "Indicates if this attendee is the current user"
|
||||
},
|
||||
"optional": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "Indicates if this attendee's attendance is optional"
|
||||
},
|
||||
"host": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "Indicates if this attendee is the host"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateUnifiedCalendarEventInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Start date and time of the calendar event with timezone information"
|
||||
},
|
||||
"end": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "End date and time of the calendar event with timezone information"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Title of the calendar event"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Detailed description of the calendar event"
|
||||
},
|
||||
"attendees": {
|
||||
"nullable": true,
|
||||
"description": "List of attendees. CAUTION: You must pass the entire array with all updated values. Any attendees not included in this array will be removed from the event.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/UpdateCalendarEventAttendee"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"nullable": true,
|
||||
"example": "accepted",
|
||||
"$ref": "#/components/schemas/CalendarEventStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RequestEmailVerificationInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+9
@@ -34,4 +34,13 @@ export class ManagedOrganizationsRepositoryFixture {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createManagedOrganization(managerOrganizationId: number, managedOrganizationId: number) {
|
||||
return this.prismaWriteClient.managedOrganization.create({
|
||||
data: {
|
||||
managerOrganizationId,
|
||||
managedOrganizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@ export type PageProps = {
|
||||
|
||||
export type LayoutProps = { params: Promise<Params>; children: React.ReactElement };
|
||||
|
||||
export { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
||||
export { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
||||
export type ReadonlyHeaders = Awaited<ReturnType<typeof import("next/headers").headers>>;
|
||||
export type ReadonlyRequestCookies = Awaited<ReturnType<typeof import("next/headers").cookies>>;
|
||||
|
||||
@@ -200,7 +200,7 @@ async function getHandler(request: NextRequest) {
|
||||
|
||||
buffer = await optimizeImage({
|
||||
buffer,
|
||||
contentType: detectContentType(buffer) ?? "image/jpeg",
|
||||
contentType: (await detectContentType(buffer)) ?? "image/jpeg",
|
||||
quality: 100,
|
||||
width: logoDefinition.w,
|
||||
height: logoDefinition.h, // optional
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { type TFunction } from "i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { List } from "@calcom/ui/components/list";
|
||||
@@ -12,10 +16,18 @@ import { StepConnectionLoader } from "../components/StepConnectionLoader";
|
||||
interface ConnectedAppStepProps {
|
||||
nextStep: () => void;
|
||||
isPageLoading: boolean;
|
||||
user: RouterOutputs["viewer"]["me"]["get"];
|
||||
}
|
||||
|
||||
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||
const { nextStep, isPageLoading } = props;
|
||||
const ConnectedVideoStepInner = ({
|
||||
t,
|
||||
setAnyInstalledVideoApps,
|
||||
user,
|
||||
}: {
|
||||
t: TFunction;
|
||||
setAnyInstalledVideoApps: Dispatch<SetStateAction<boolean>>;
|
||||
user: RouterOutputs["viewer"]["me"]["get"];
|
||||
}) => {
|
||||
const { data: queryConnectedVideoApps, isPending } = trpc.viewer.apps.integrations.useQuery({
|
||||
variant: "conferencing",
|
||||
onlyInstalled: false,
|
||||
@@ -30,47 +42,59 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||
sortByMostPopular: true,
|
||||
sortByInstalledFirst: true,
|
||||
});
|
||||
const { data } = useMeQuery();
|
||||
const { t } = useLocale();
|
||||
|
||||
const metadata = userMetadata.parse(data?.metadata);
|
||||
|
||||
const hasAnyInstalledVideoApps = queryConnectedVideoApps?.items.some(
|
||||
(item) => item.userCredentialIds.length > 0
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setAnyInstalledVideoApps(Boolean(hasAnyInstalledVideoApps));
|
||||
}, [hasAnyInstalledVideoApps, setAnyInstalledVideoApps]);
|
||||
|
||||
if (isPending) {
|
||||
return <StepConnectionLoader />;
|
||||
}
|
||||
|
||||
const result = userMetadata.safeParse(user.metadata);
|
||||
if (!result.success) {
|
||||
return <StepConnectionLoader />;
|
||||
}
|
||||
const { data: metadata } = result;
|
||||
const defaultConferencingApp = metadata?.defaultConferencingApp?.appSlug;
|
||||
return (
|
||||
<>
|
||||
{!isPending && (
|
||||
<List className="bg-default border-subtle divide-subtle scroll-bar mx-1 max-h-[45vh] divide-y !overflow-y-scroll rounded-md border p-0 sm:mx-0">
|
||||
{queryConnectedVideoApps?.items &&
|
||||
queryConnectedVideoApps?.items.map((item) => {
|
||||
if (item.slug === "daily-video") return null; // we dont want to show daily here as it is installed by default
|
||||
return (
|
||||
<li key={item.name}>
|
||||
{item.name && item.logo && (
|
||||
<AppConnectionItem
|
||||
type={item.type}
|
||||
title={item.name}
|
||||
isDefault={item.slug === defaultConferencingApp}
|
||||
description={item.description}
|
||||
dependencyData={item.dependencyData}
|
||||
logo={item.logo}
|
||||
slug={item.slug}
|
||||
installed={item.userCredentialIds.length > 0}
|
||||
defaultInstall={
|
||||
!defaultConferencingApp && item.appData?.location?.linkType === "dynamic"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
<List className="bg-default border-subtle divide-subtle scroll-bar mx-1 max-h-[45vh] divide-y !overflow-y-scroll rounded-md border p-0 sm:mx-0">
|
||||
{queryConnectedVideoApps?.items &&
|
||||
queryConnectedVideoApps?.items.map((item) => {
|
||||
if (item.slug === "daily-video") return null; // we dont want to show daily here as it is installed by default
|
||||
return (
|
||||
<li key={item.name}>
|
||||
{item.name && item.logo && (
|
||||
<AppConnectionItem
|
||||
type={item.type}
|
||||
title={item.name}
|
||||
isDefault={item.slug === defaultConferencingApp}
|
||||
description={item.description}
|
||||
dependencyData={item.dependencyData}
|
||||
logo={item.logo}
|
||||
slug={item.slug}
|
||||
installed={item.userCredentialIds.length > 0}
|
||||
defaultInstall={!defaultConferencingApp && item.appData?.location?.linkType === "dynamic"}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
{isPending && <StepConnectionLoader />}
|
||||
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||
const { nextStep, isPageLoading, user } = props;
|
||||
const { t } = useLocale();
|
||||
const [hasAnyInstalledVideoApps, setAnyInstalledVideoApps] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<ConnectedVideoStepInner setAnyInstalledVideoApps={setAnyInstalledVideoApps} t={t} user={user} />
|
||||
<Button
|
||||
EndIcon="arrow-right"
|
||||
data-testid="save-video-button"
|
||||
|
||||
@@ -10,6 +10,7 @@ import { md } from "@calcom/lib/markdownIt";
|
||||
import { telemetryEventTypes } from "@calcom/lib/telemetry";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { UserAvatar } from "@calcom/ui/components/avatar";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Editor } from "@calcom/ui/components/editor";
|
||||
@@ -21,8 +22,11 @@ type FormData = {
|
||||
bio: string;
|
||||
};
|
||||
|
||||
const UserProfile = () => {
|
||||
const [user] = trpc.viewer.me.get.useSuspenseQuery();
|
||||
interface UserProfileProps {
|
||||
user: RouterOutputs["viewer"]["me"]["get"];
|
||||
}
|
||||
|
||||
const UserProfile = ({ user }: UserProfileProps) => {
|
||||
const { t } = useLocale();
|
||||
const avatarRef = useRef<HTMLInputElement>(null);
|
||||
const { setValue, handleSubmit, getValues } = useForm<FormData>({
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTelemetry } from "@calcom/lib/hooks/useTelemetry";
|
||||
import { telemetryEventTypes } from "@calcom/lib/telemetry";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Input } from "@calcom/ui/components/form";
|
||||
|
||||
@@ -21,11 +22,11 @@ import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||
interface IUserSettingsProps {
|
||||
nextStep: () => void;
|
||||
hideUsername?: boolean;
|
||||
user: RouterOutputs["viewer"]["me"]["get"];
|
||||
}
|
||||
|
||||
const UserSettings = (props: IUserSettingsProps) => {
|
||||
const { nextStep } = props;
|
||||
const [user] = trpc.viewer.me.get.useSuspenseQuery();
|
||||
const { nextStep, user } = props;
|
||||
const { t } = useLocale();
|
||||
const { setTimezone: setSelectedTimeZone, timezone: selectedTimeZone } = useTimePreferences();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { SearchParams } from "app/_types";
|
||||
import { type Params } from "app/_types";
|
||||
import type { ReadonlyHeaders, ReadonlyRequestCookies } from "app/_types";
|
||||
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>) =>
|
||||
new Proxy(object, {
|
||||
|
||||
@@ -162,20 +162,20 @@ const OnboardingPage = (props: PageProps) => {
|
||||
<StepCard>
|
||||
<Suspense fallback={<Icon name="loader" />}>
|
||||
{currentStep === "user-settings" && (
|
||||
<UserSettings nextStep={goToNextStep} hideUsername={from === "signup"} />
|
||||
<UserSettings nextStep={goToNextStep} hideUsername={from === "signup"} user={user} />
|
||||
)}
|
||||
{currentStep === "connected-calendar" && (
|
||||
<ConnectedCalendars nextStep={goToNextStep} isPageLoading={isNextStepLoading} />
|
||||
)}
|
||||
|
||||
{currentStep === "connected-video" && (
|
||||
<ConnectedVideoStep nextStep={goToNextStep} isPageLoading={isNextStepLoading} />
|
||||
<ConnectedVideoStep nextStep={goToNextStep} isPageLoading={isNextStepLoading} user={user} />
|
||||
)}
|
||||
|
||||
{currentStep === "setup-availability" && (
|
||||
<SetupAvailability nextStep={goToNextStep} defaultScheduleId={user.defaultScheduleId} />
|
||||
)}
|
||||
{currentStep === "user-profile" && <UserProfile />}
|
||||
{currentStep === "user-profile" && <UserProfile user={user} />}
|
||||
</Suspense>
|
||||
</StepCard>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.5.16",
|
||||
"version": "5.5.17",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -106,7 +106,7 @@
|
||||
"memory-cache": "^0.2.0",
|
||||
"micro": "^10.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"next": "^15.3.0",
|
||||
"next": "^15.4.5",
|
||||
"next-auth": "^4.22.1",
|
||||
"next-axiom": "^0.17.0",
|
||||
"next-collect": "^0.2.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IncomingMessage } from "http";
|
||||
import type { NextPageContext } from "next";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import type { AppContextType } from "next/dist/shared/lib/utils";
|
||||
import React from "react";
|
||||
import CacheProvider from "react-inlinesvg/provider";
|
||||
|
||||
@@ -32,8 +32,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
MyApp.getInitialProps = async (ctx: AppContextType) => {
|
||||
const { req } = ctx.ctx;
|
||||
MyApp.getInitialProps = async ({ ctx }: { ctx: NextPageContext }) => {
|
||||
const { req } = ctx;
|
||||
|
||||
let newLocale = "en";
|
||||
|
||||
|
||||
+5312
-7363
File diff suppressed because it is too large
Load Diff
@@ -127,6 +127,7 @@ Below is a list of props that can be passed to the booker atom.
|
||||
| timeZones | No | Array of valid IANA timezones to be used in the booker. Eg. ["Asia/Kolkata", "Europe/London"] |
|
||||
| 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. |
|
||||
| showNoAvailabilityDialog | No | Boolean indicating if the no availability dialog should be shown, defaults to true. |
|
||||
|
||||
## Styling
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@ import { parse } from "accept-language-parser";
|
||||
import { lookup } from "bcp-47-match";
|
||||
import type { GetTokenParams } 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";
|
||||
|
||||
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
|
||||
* `next-auth`.
|
||||
|
||||
@@ -10,6 +10,7 @@ import dayjs from "@calcom/dayjs";
|
||||
import PoweredBy from "@calcom/ee/components/PoweredBy";
|
||||
import { updateEmbedBookerState } from "@calcom/embed-core/src/embed-iframe";
|
||||
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 { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
||||
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 { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
|
||||
import framerFeatures from "./framer-features";
|
||||
import { useBookerStore } from "./store";
|
||||
import type { BookerProps, WrappedBookerProps } from "./types";
|
||||
import { isBookingDryRun } from "./utils/isBookingDryRun";
|
||||
import { isTimeSlotAvailable } from "./utils/isTimeslotAvailable";
|
||||
@@ -81,12 +81,18 @@ const BookerComponent = ({
|
||||
timeZones,
|
||||
eventMetaChildren,
|
||||
roundRobinHideOrgAndTeam,
|
||||
showNoAvailabilityDialog,
|
||||
}: BookerProps & WrappedBookerProps) => {
|
||||
const searchParams = useCompatSearchParams();
|
||||
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 {
|
||||
shouldShowFormInDialog,
|
||||
hasDarkBackground,
|
||||
@@ -99,12 +105,15 @@ const BookerComponent = ({
|
||||
bookerLayouts,
|
||||
} = bookerLayout;
|
||||
|
||||
const [seatedEventData, setSeatedEventData] = useBookerStore(
|
||||
const [seatedEventData, setSeatedEventData] = useBookerStoreContext(
|
||||
(state) => [state.seatedEventData, state.setSeatedEventData],
|
||||
shallow
|
||||
);
|
||||
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(
|
||||
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
|
||||
@@ -183,6 +192,10 @@ const BookerComponent = ({
|
||||
(bookerState === "booking" || (bookerState === "selecting_time" && skipConfirmStep))
|
||||
);
|
||||
|
||||
const onAvailableTimeSlotSelect = (time: string) => {
|
||||
setSelectedTimeslot(time);
|
||||
};
|
||||
|
||||
updateEmbedBookerState({ bookerState, slotsQuery: schedule });
|
||||
|
||||
useEffect(() => {
|
||||
@@ -219,6 +232,7 @@ const BookerComponent = ({
|
||||
return bookerState === "booking" ? (
|
||||
<BookEventForm
|
||||
key={key}
|
||||
timeslot={selectedTimeslot}
|
||||
shouldRenderCaptcha={shouldRenderCaptcha}
|
||||
onCancel={() => {
|
||||
setSelectedTimeslot(null);
|
||||
@@ -311,9 +325,7 @@ const BookerComponent = ({
|
||||
return (
|
||||
<>
|
||||
{event.data && !isPlatform ? <BookingPageTagManager eventType={event.data} /> : <></>}
|
||||
|
||||
{(isBookingDryRunProp || isBookingDryRun(searchParams)) && <DryRunMessage isEmbed={isEmbed} />}
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
// In a popup embed, if someone clicks outside the main(having main class or main tag), it closes the embed
|
||||
@@ -393,6 +405,7 @@ const BookerComponent = ({
|
||||
/>
|
||||
)}
|
||||
<EventMeta
|
||||
selectedTimeslot={selectedTimeslot}
|
||||
classNames={{
|
||||
eventMetaContainer: customClassNames?.eventMetaCustomClassNames?.eventMetaContainer,
|
||||
eventMetaTitle: customClassNames?.eventMetaCustomClassNames?.eventMetaTitle,
|
||||
@@ -417,6 +430,7 @@ const BookerComponent = ({
|
||||
slots={schedule?.data?.slots}
|
||||
isLoading={schedule.isPending}
|
||||
scrollToTimeSlots={scrollToTimeSlots}
|
||||
showNoAvailabilityDialog={showNoAvailabilityDialog}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -445,6 +459,7 @@ const BookerComponent = ({
|
||||
slots={schedule?.data?.slots}
|
||||
isLoading={schedule.isPending}
|
||||
scrollToTimeSlots={scrollToTimeSlots}
|
||||
showNoAvailabilityDialog={showNoAvailabilityDialog}
|
||||
/>
|
||||
</BookerSection>
|
||||
|
||||
@@ -477,6 +492,7 @@ const BookerComponent = ({
|
||||
ref={timeslotsRef}
|
||||
{...fadeInLeft}>
|
||||
<AvailableTimeSlots
|
||||
onAvailableTimeSlotSelect={onAvailableTimeSlotSelect}
|
||||
customClassNames={customClassNames?.availableTimeSlotsCustomClassNames}
|
||||
extraDays={extraDays}
|
||||
limitHeight={layout === BookerLayouts.MONTH_VIEW}
|
||||
@@ -549,7 +565,6 @@ const BookerComponent = ({
|
||||
</m.span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<>
|
||||
{verifyCode && formEmail ? (
|
||||
<VerifyCodeDialog
|
||||
@@ -568,7 +583,6 @@ const BookerComponent = ({
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
|
||||
<BookFormAsModal
|
||||
onCancel={() => setSelectedTimeslot(null)}
|
||||
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 "@calcom/lib/__mocks__/logger";
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import "@calcom/dayjs/__mocks__";
|
||||
import "@calcom/features/auth/Turnstile";
|
||||
|
||||
import { Booker } from "../Booker";
|
||||
import { useBookerStore } from "../store";
|
||||
import type { BookerState } from "../types";
|
||||
import { render, screen } from "./test-utils";
|
||||
|
||||
vi.mock("framer-motion", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
const actual = (await importOriginal()) as any;
|
||||
return {
|
||||
...actual,
|
||||
};
|
||||
@@ -87,27 +86,6 @@ vi.mock("@calcom/atoms/hooks/useIsPlatform", () => ({
|
||||
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
|
||||
const defaultProps = {
|
||||
username: "testuser",
|
||||
@@ -188,7 +166,7 @@ const defaultProps = {
|
||||
describe("Booker", () => {
|
||||
beforeEach(() => {
|
||||
constantsScenarios.set({
|
||||
PUBLIC_QUICK_AVAILABILITY_ROLLOUT: 100,
|
||||
PUBLIC_QUICK_AVAILABILITY_ROLLOUT: "100",
|
||||
POWERED_BY_URL: "https://go.cal.com/booking",
|
||||
APP_NAME: "Cal.com",
|
||||
});
|
||||
@@ -196,24 +174,13 @@ describe("Booker", () => {
|
||||
});
|
||||
|
||||
it("should render null when in loading state", () => {
|
||||
useBookerStore.setState({
|
||||
...mockStoreState,
|
||||
state: "loading",
|
||||
const { container } = render(<Booker {...defaultProps} />, {
|
||||
mockStore: { state: "loading" },
|
||||
});
|
||||
|
||||
const { container } = render(<Booker {...defaultProps} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
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 = {
|
||||
...defaultProps,
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -242,12 +216,10 @@ describe("Booker", () => {
|
||||
invalidate: mockInvalidate,
|
||||
},
|
||||
};
|
||||
useBookerStore.setState({
|
||||
...mockStoreState,
|
||||
state: "booking",
|
||||
});
|
||||
|
||||
render(<Booker {...propsWithInvalidate} />);
|
||||
render(<Booker {...propsWithInvalidate} />, {
|
||||
mockStore: { state: "booking" },
|
||||
});
|
||||
screen.logTestingPlaygroundURL();
|
||||
// Trigger form cancel
|
||||
const cancelButton = screen.getByRole("button", { name: /cancel/i });
|
||||
@@ -267,12 +239,9 @@ describe("Booker", () => {
|
||||
},
|
||||
};
|
||||
|
||||
useBookerStore.setState({
|
||||
...mockStoreState,
|
||||
state: "booking",
|
||||
render(<Booker {...propsWithQuickChecks} />, {
|
||||
mockStore: { state: "booking" },
|
||||
});
|
||||
|
||||
render(<Booker {...propsWithQuickChecks} />);
|
||||
const bookEventForm = screen.getByTestId("book-event-form");
|
||||
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 { 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 { BookerEvent } from "@calcom/features/bookings/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 { AvailableTimesHeader } from "../../components/AvailableTimesHeader";
|
||||
import { useBookerStore } from "../store";
|
||||
import type { useScheduleForEventReturnType } from "../utils/event";
|
||||
import { getQueryParam } from "../utils/query-param";
|
||||
|
||||
@@ -49,6 +49,7 @@ type AvailableTimeSlotsProps = {
|
||||
*/
|
||||
unavailableTimeSlots: string[];
|
||||
confirmButtonDisabled?: boolean;
|
||||
onAvailableTimeSlotSelect: (time: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -72,17 +73,17 @@ export const AvailableTimeSlots = ({
|
||||
unavailableTimeSlots,
|
||||
confirmButtonDisabled,
|
||||
confirmStepClassNames,
|
||||
onAvailableTimeSlotSelect,
|
||||
...props
|
||||
}: AvailableTimeSlotsProps) => {
|
||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
|
||||
|
||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
||||
const setSeatedEventData = useBookerStore((state) => state.setSeatedEventData);
|
||||
const setSeatedEventData = useBookerStoreContext((state) => state.setSeatedEventData);
|
||||
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 containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const { setTentativeSelectedTimeslots, tentativeSelectedTimeslots } = useBookerStore((state) => ({
|
||||
const { setTentativeSelectedTimeslots, tentativeSelectedTimeslots } = useBookerStoreContext((state) => ({
|
||||
setTentativeSelectedTimeslots: state.setTentativeSelectedTimeslots,
|
||||
tentativeSelectedTimeslots: state.tentativeSelectedTimeslots,
|
||||
}));
|
||||
@@ -146,7 +147,9 @@ export const AvailableTimeSlots = ({
|
||||
showAvailableSeatsCount,
|
||||
});
|
||||
}
|
||||
setSelectedTimeslot(time);
|
||||
|
||||
onAvailableTimeSlotSelect(time);
|
||||
|
||||
const isTimeSlotAvailable = !unavailableTimeSlots.includes(time);
|
||||
if (skipConfirmStep && isTimeSlotAvailable) {
|
||||
onSubmit(time);
|
||||
@@ -156,12 +159,12 @@ export const AvailableTimeSlots = ({
|
||||
[
|
||||
onSubmit,
|
||||
setSeatedEventData,
|
||||
setSelectedTimeslot,
|
||||
skipConfirmStep,
|
||||
showAvailableSeatsCount,
|
||||
unavailableTimeSlots,
|
||||
schedule,
|
||||
setTentativeSelectedTimeslots,
|
||||
onAvailableTimeSlotSelect,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
|
||||
import type { FieldError } from "react-hook-form";
|
||||
|
||||
import { useIsPlatformBookerEmbed } from "@calcom/atoms/hooks/useIsPlatformBookerEmbed";
|
||||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||
import ServerTrans from "@calcom/lib/components/ServerTrans";
|
||||
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 { Form } from "@calcom/ui/components/form";
|
||||
|
||||
import { useBookerStore } from "../../store";
|
||||
import { formatEventFromTime } from "../../utils/dates";
|
||||
import { useBookerTime } from "../hooks/useBookerTime";
|
||||
import type { UseBookingFormReturnType } from "../hooks/useBookingForm";
|
||||
@@ -43,6 +43,7 @@ type BookEventFormProps = {
|
||||
confirmButton?: string;
|
||||
backButton?: string;
|
||||
};
|
||||
timeslot: string | null;
|
||||
};
|
||||
|
||||
export const BookEventForm = ({
|
||||
@@ -62,6 +63,7 @@ export const BookEventForm = ({
|
||||
shouldRenderCaptcha,
|
||||
confirmButtonDisabled,
|
||||
classNames,
|
||||
timeslot,
|
||||
}: Omit<BookEventFormProps, "event"> & {
|
||||
eventQuery: {
|
||||
isError: boolean;
|
||||
@@ -70,12 +72,11 @@ export const BookEventForm = ({
|
||||
};
|
||||
}) => {
|
||||
const eventType = eventQuery.data;
|
||||
const setFormValues = useBookerStore((state) => state.setFormValues);
|
||||
const bookingData = useBookerStore((state) => state.bookingData);
|
||||
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
||||
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
||||
const username = useBookerStore((state) => state.username);
|
||||
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
|
||||
const setFormValues = useBookerStoreContext((state) => state.setFormValues);
|
||||
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
|
||||
const username = useBookerStoreContext((state) => state.username);
|
||||
const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
|
||||
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
|
||||
const { timeFormat, timezone } = useBookerTime();
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ import React from "react";
|
||||
|
||||
import { useEventTypeById } from "@calcom/atoms/hooks/event-types/private/useEventTypeById";
|
||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { DialogContent } from "@calcom/ui/components/dialog";
|
||||
|
||||
import { getDurationFormatted } from "../../../components/event-meta/Duration";
|
||||
import { useBookerStore } from "../../store";
|
||||
import { FromTime } from "../../utils/dates";
|
||||
import { useEvent } from "../../utils/event";
|
||||
import { useBookerTime } from "../hooks/useBookerTime";
|
||||
@@ -27,7 +27,7 @@ const PlatformBookEventFormWrapper = ({
|
||||
onCancel: () => void;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const eventId = useBookerStore((state) => state.eventId);
|
||||
const eventId = useBookerStoreContext((state) => state.eventId);
|
||||
const { data } = useEventTypeById(eventId);
|
||||
|
||||
return (
|
||||
@@ -44,8 +44,8 @@ export const BookEventFormWrapperComponent = ({
|
||||
eventLength?: number;
|
||||
}) => {
|
||||
const { i18n, t } = useLocale();
|
||||
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
|
||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
||||
const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
|
||||
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||
const { timeFormat, timezone } = useBookerTime();
|
||||
if (!selectedTimeslot) {
|
||||
return null;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { vi, afterEach } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
||||
|
||||
import { render } from "../__tests__/test-utils";
|
||||
import { DatePicker } from "./DatePicker";
|
||||
|
||||
vi.mock("@calcom/features/calendars/DatePicker", () => {
|
||||
@@ -18,7 +19,7 @@ const noop = () => {
|
||||
|
||||
describe("Tests for DatePicker Component", () => {
|
||||
afterEach(() => {
|
||||
DatePickerComponent.mockClear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
// there'll be one slot open on this day, next month.
|
||||
const slotDate = dayjs().add("1", "month");
|
||||
const slotDate = dayjs().add(1, "month");
|
||||
render(
|
||||
<DatePicker
|
||||
event={{}}
|
||||
@@ -72,7 +73,12 @@ describe("Tests for DatePicker Component", () => {
|
||||
],
|
||||
}}
|
||||
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
|
||||
// by setting the browsingDate to the next month.
|
||||
@@ -94,13 +100,18 @@ describe("Tests for DatePicker Component", () => {
|
||||
}
|
||||
}
|
||||
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
|
||||
// by setting the browsingDate to the next month.
|
||||
expect(DatePickerComponent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
browsingDate: dayjs().add("1", "month").startOf("month"),
|
||||
browsingDate: dayjs().add(1, "month").startOf("month"),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { shallow } from "zustand/shallow";
|
||||
|
||||
import type { 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 { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
||||
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 { Slots } from "../../types";
|
||||
import { useBookerStore } from "../store";
|
||||
|
||||
const useMoveToNextMonthOnNoAvailability = ({
|
||||
browsingDate,
|
||||
@@ -57,6 +57,7 @@ export const DatePicker = ({
|
||||
isLoading,
|
||||
classNames,
|
||||
scrollToTimeSlots,
|
||||
showNoAvailabilityDialog,
|
||||
}: {
|
||||
event: {
|
||||
data?: {
|
||||
@@ -72,14 +73,15 @@ export const DatePicker = ({
|
||||
isLoading?: boolean;
|
||||
classNames?: DatePickerClassNames;
|
||||
scrollToTimeSlots?: () => void;
|
||||
showNoAvailabilityDialog?: boolean;
|
||||
}) => {
|
||||
const { i18n } = useLocale();
|
||||
const [month, selectedDate, layout] = useBookerStore(
|
||||
const [month, selectedDate, layout] = useBookerStoreContext(
|
||||
(state) => [state.month, state.selectedDate, state.layout],
|
||||
shallow
|
||||
);
|
||||
|
||||
const [setSelectedDate, setMonth, setDayCount] = useBookerStore(
|
||||
const [setSelectedDate, setMonth, setDayCount] = useBookerStoreContext(
|
||||
(state) => [state.setSelectedDate, state.setMonth, state.setDayCount],
|
||||
shallow
|
||||
);
|
||||
@@ -148,6 +150,7 @@ export const DatePicker = ({
|
||||
scrollToTimeSlots={scrollToTimeSlots}
|
||||
periodData={periodData}
|
||||
isCompact={isCompact}
|
||||
showNoAvailabilityDialog={showNoAvailabilityDialog}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { shallow } from "zustand/shallow";
|
||||
import { Timezone as PlatformTimezoneSelect } from "@calcom/atoms/timezone";
|
||||
import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
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 { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText";
|
||||
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 { fadeInUp } from "../config";
|
||||
import { useBookerStore } from "../store";
|
||||
import { FromToTime } from "../utils/dates";
|
||||
import { useBookerTime } from "./hooks/useBookerTime";
|
||||
|
||||
@@ -54,7 +54,8 @@ export const EventMeta = ({
|
||||
locale,
|
||||
timeZones,
|
||||
children,
|
||||
roundRobinHideOrgAndTeam,
|
||||
selectedTimeslot,
|
||||
roundRobinHideOrgAndTeam
|
||||
}: {
|
||||
event?: Pick<
|
||||
BookerEvent,
|
||||
@@ -91,17 +92,17 @@ export const EventMeta = ({
|
||||
locale?: string | null;
|
||||
timeZones?: Timezone[];
|
||||
children?: React.ReactNode;
|
||||
selectedTimeslot: string | null;
|
||||
roundRobinHideOrgAndTeam?: boolean;
|
||||
}) => {
|
||||
const { timeFormat, timezone } = useBookerTime();
|
||||
const [setTimezone] = useTimePreferences((state) => [state.setTimezone]);
|
||||
const [setBookerStoreTimezone] = useBookerStore((state) => [state.setTimezone], shallow);
|
||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
||||
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
|
||||
const bookerState = useBookerStore((state) => state.state);
|
||||
const bookingData = useBookerStore((state) => state.bookingData);
|
||||
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
||||
const [seatedEventData, setSeatedEventData] = useBookerStore(
|
||||
const [setBookerStoreTimezone] = useBookerStoreContext((state) => [state.setTimezone], shallow);
|
||||
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||
const bookerState = useBookerStoreContext((state) => state.state);
|
||||
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
|
||||
const [seatedEventData, setSeatedEventData] = useBookerStoreContext(
|
||||
(state) => [state.seatedEventData, state.setSeatedEventData],
|
||||
shallow
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { shallow } from "zustand/shallow";
|
||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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 { TimeFormatToggle } from "../../components/TimeFormatToggle";
|
||||
import { useBookerStore } from "../store";
|
||||
import type { BookerLayout } from "../types";
|
||||
|
||||
export function Header({
|
||||
@@ -36,10 +36,10 @@ export function Header({
|
||||
}) {
|
||||
const { t, i18n } = useLocale();
|
||||
const isEmbed = useIsEmbed();
|
||||
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
|
||||
const selectedDateString = useBookerStore((state) => state.selectedDate);
|
||||
const setSelectedDate = useBookerStore((state) => state.setSelectedDate);
|
||||
const addToSelectedDate = useBookerStore((state) => state.addToSelectedDate);
|
||||
const [layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow);
|
||||
const selectedDateString = useBookerStoreContext((state) => state.selectedDate);
|
||||
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
|
||||
const addToSelectedDate = useBookerStoreContext((state) => state.addToSelectedDate);
|
||||
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
|
||||
const today = dayjs();
|
||||
const selectedDate = selectedDateString ? dayjs(selectedDateString) : today;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useMemo, useEffect } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||
import { Calendar } from "@calcom/features/calendars/weeklyview";
|
||||
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
|
||||
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
|
||||
import { useBookerStore } from "../store";
|
||||
import type { useScheduleForEventReturnType } from "../utils/event";
|
||||
import { getQueryParam } from "../utils/query-param";
|
||||
import { useOverlayCalendarStore } from "./OverlayCalendar/store";
|
||||
@@ -25,9 +25,9 @@ export const LargeCalendar = ({
|
||||
data?: Pick<BookerEvent, "length"> | null;
|
||||
};
|
||||
}) => {
|
||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
||||
const selectedEventDuration = useBookerStore((state) => state.selectedDuration);
|
||||
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
|
||||
const setSelectedTimeslot = useBookerStoreContext((state) => state.setSelectedTimeslot);
|
||||
const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||
const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates);
|
||||
const displayOverlay =
|
||||
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
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 { Button } from "@calcom/ui/components/button";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
|
||||
import { useBookerStore } from "../../store";
|
||||
import { useOverlayCalendarStore } from "./store";
|
||||
|
||||
interface OverlayCalendarSwitchProps {
|
||||
@@ -20,7 +20,7 @@ export function OverlayCalendarSwitch({ enabled, hasSession, onStateChange }: Ov
|
||||
const setCalendarSettingsOverlay = useOverlayCalendarStore(
|
||||
(state) => state.setCalendarSettingsOverlayModal
|
||||
);
|
||||
const layout = useBookerStore((state) => state.layout);
|
||||
const layout = useBookerStoreContext((state) => state.layout);
|
||||
const switchEnabled = enabled;
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { MotionProps } from "framer-motion";
|
||||
import { m } from "framer-motion";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
|
||||
import { useBookerStore } from "../store";
|
||||
import type { BookerAreas, BookerLayout } from "../types";
|
||||
|
||||
/**
|
||||
@@ -44,7 +44,7 @@ export const BookerSection = forwardRef<HTMLDivElement, BookerSectionProps>(func
|
||||
{ children, area, visible, className, ...props },
|
||||
ref
|
||||
) {
|
||||
const layout = useBookerStore((state) => state.layout);
|
||||
const layout = useBookerStoreContext((state) => state.layout);
|
||||
let gridClassName: string;
|
||||
|
||||
if (typeof area === "string") {
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useEffect, useRef } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
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 useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import type { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
import { defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { extraDaysConfig } from "../../config";
|
||||
import { useBookerStore } from "../../store";
|
||||
import type { BookerLayout } from "../../types";
|
||||
import { validateLayout } from "../../utils/layout";
|
||||
import { getQueryParam } from "../../utils/query-param";
|
||||
@@ -18,7 +18,7 @@ export type UseBookerLayoutType = ReturnType<typeof useBookerLayout>;
|
||||
export const useBookerLayout = (
|
||||
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 isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const isTablet = useMediaQuery("(max-width: 1024px)");
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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 { getBookerTimezone } from "../../utils/getBookerTimezone";
|
||||
|
||||
export const useBookerTime = () => {
|
||||
const [timezoneFromBookerStore] = useBookerStore((state) => [state.timezone], shallow);
|
||||
const [timezoneFromBookerStore] = useBookerStoreContext((state) => [state.timezone], shallow);
|
||||
const { timezone: timezoneFromTimePreferences, timeFormat } = useTimePreferences();
|
||||
const timezone = getBookerTimezone({
|
||||
storeTimezone: timezoneFromBookerStore,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||
import { useHandleBookEvent } from "@calcom/atoms/hooks/bookings/useHandleBookEvent";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
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 { updateQueryParam, getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
||||
import { createBooking, createRecurringBooking, createInstantBooking } from "@calcom/features/bookings/lib";
|
||||
@@ -117,21 +118,21 @@ export const useBookings = ({
|
||||
isBookingDryRun,
|
||||
}: IUseBookings) => {
|
||||
const router = useRouter();
|
||||
const eventSlug = useBookerStore((state) => state.eventSlug);
|
||||
const eventTypeId = useBookerStore((state) => state.eventId);
|
||||
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
|
||||
const eventSlug = useBookerStoreContext((state) => state.eventSlug);
|
||||
const eventTypeId = useBookerStoreContext((state) => state.eventId);
|
||||
const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
|
||||
|
||||
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
||||
const rescheduledBy = useBookerStore((state) => state.rescheduledBy);
|
||||
const bookingData = useBookerStore((state) => state.bookingData);
|
||||
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
||||
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
|
||||
const rescheduledBy = useBookerStoreContext((state) => state.rescheduledBy);
|
||||
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||
const timeslot = useBookerStoreContext((state) => state.selectedTimeslot);
|
||||
const { t } = useLocale();
|
||||
const bookingSuccessRedirect = useBookingSuccessRedirect();
|
||||
const bookerFormErrorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [instantMeetingTokenExpiryTime, setExpiryTime] = useState<Date | undefined>();
|
||||
const [instantVideoMeetingUrl, setInstantVideoMeetingUrl] = useState<string | undefined>();
|
||||
const duration = useBookerStore((state) => state.selectedDuration);
|
||||
const duration = useBookerStoreContext((state) => state.selectedDuration);
|
||||
|
||||
const isRescheduling = !!rescheduleUid && !!bookingData;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
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 { isBookingDryRun } from "@calcom/features/bookings/Booker/utils/isBookingDryRun";
|
||||
import {
|
||||
@@ -78,10 +78,10 @@ const useQuickAvailabilityChecks = ({
|
||||
export type UseSlotsReturnType = ReturnType<typeof useSlots>;
|
||||
|
||||
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 [selectedTimeslot, setSelectedTimeslot, tentativeSelectedTimeslots, setTentativeSelectedTimeslots] =
|
||||
useBookerStore(
|
||||
useBookerStoreContext(
|
||||
(state) => [
|
||||
state.selectedTimeslot,
|
||||
state.setSelectedTimeslot,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { updateQueryParam, getQueryParam, removeQueryParam } from "./utils/query
|
||||
* Arguments passed into store initializer, containing
|
||||
* the event data.
|
||||
*/
|
||||
type StoreInitializeType = {
|
||||
export type StoreInitializeType = {
|
||||
username: string;
|
||||
eventSlug: string;
|
||||
// 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
|
||||
* 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.
|
||||
* Creates a new booker store instance
|
||||
*/
|
||||
export const useBookerStore = createWithEqualityFn<BookerStore>((set, get) => ({
|
||||
state: "loading",
|
||||
setState: (state: BookerState) => set({ state }),
|
||||
layout: BookerLayouts.MONTH_VIEW,
|
||||
setLayout: (layout: BookerLayout) => {
|
||||
// If we switch to a large layout and don't have a date selected yet,
|
||||
// we selected it here, so week title is rendered properly.
|
||||
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"));
|
||||
export const createBookerStore = () =>
|
||||
createWithEqualityFn<BookerStore>((set, get) => ({
|
||||
state: "loading",
|
||||
setState: (state: BookerState) => set({ state }),
|
||||
layout: BookerLayouts.MONTH_VIEW,
|
||||
setLayout: (layout: BookerLayout) => {
|
||||
// If we switch to a large layout and don't have a date selected yet,
|
||||
// we selected it here, so week title is rendered properly.
|
||||
if (["week_view", "column_view"].includes(layout) && !get().selectedDate) {
|
||||
set({ selectedDate: dayjs().format("YYYY-MM-DD") });
|
||||
}
|
||||
}
|
||||
},
|
||||
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) {
|
||||
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 });
|
||||
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,
|
||||
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;
|
||||
const currentSelection = dayjs(get().selectedDate);
|
||||
const newSelection = dayjs(selectedDate);
|
||||
set({ selectedDate });
|
||||
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
|
||||
updateQueryParam("date", selectedDate ?? "");
|
||||
}
|
||||
|
||||
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({
|
||||
// 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) {
|
||||
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,
|
||||
eventSlug,
|
||||
month,
|
||||
eventId,
|
||||
org,
|
||||
rescheduleUid,
|
||||
rescheduledBy,
|
||||
bookingUid,
|
||||
bookingData,
|
||||
layout: layout || BookerLayouts.MONTH_VIEW,
|
||||
isTeamEvent: isTeamEvent || false,
|
||||
rescheduleUid = null,
|
||||
rescheduledBy = null,
|
||||
bookingUid = null,
|
||||
bookingData = null,
|
||||
layout,
|
||||
isTeamEvent,
|
||||
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),
|
||||
org,
|
||||
isInstantMeeting,
|
||||
timezone = null,
|
||||
teamMemberEmail,
|
||||
crmOwnerRecordType,
|
||||
crmAppSlug,
|
||||
crmRecordId,
|
||||
isPlatform,
|
||||
allowUpdatingUrlParams,
|
||||
});
|
||||
isPlatform = false,
|
||||
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({
|
||||
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 the user reschedules a booking right after the confirmation page.
|
||||
// In that case the time would still be store in the store, this way we
|
||||
// force clear this.
|
||||
// Also, fetch the original booking duration if user is rescheduling and
|
||||
// update the selectedDuration
|
||||
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 (durationConfig?.includes(Number(getQueryParam("duration")))) {
|
||||
set({
|
||||
selectedDuration: Number(getQueryParam("duration")),
|
||||
});
|
||||
} else {
|
||||
removeQueryParam("duration");
|
||||
}
|
||||
}
|
||||
if (month) set({ month });
|
||||
|
||||
if (isInstantMeeting) {
|
||||
const month = dayjs().format("YYYY-MM");
|
||||
const selectedDate = dayjs().format("YYYY-MM-DD");
|
||||
const selectedTimeslot = new Date().toISOString();
|
||||
set({
|
||||
month,
|
||||
selectedDate,
|
||||
selectedTimeslot,
|
||||
isInstantMeeting,
|
||||
});
|
||||
// Unset selected timeslot if user is rescheduling. This could happen
|
||||
// if the user reschedules a booking right after the confirmation page.
|
||||
// In that case the time would still be store in the store, this way we
|
||||
// force clear this.
|
||||
// Also, fetch the original booking duration if user is rescheduling and
|
||||
// update the selectedDuration
|
||||
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 (!isPlatform || allowUpdatingUrlParams) {
|
||||
updateQueryParam("month", month);
|
||||
updateQueryParam("date", selectedDate ?? "");
|
||||
if (isInstantMeeting) {
|
||||
const month = dayjs().format("YYYY-MM");
|
||||
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);
|
||||
}
|
||||
}
|
||||
//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);
|
||||
}
|
||||
},
|
||||
formValues: {},
|
||||
setFormValues: (formValues: Record<string, any>) => {
|
||||
set({ formValues });
|
||||
},
|
||||
org: null,
|
||||
setOrg: (org: string | null | undefined) => {
|
||||
set({ org });
|
||||
},
|
||||
isPlatform: false,
|
||||
allowUpdatingUrlParams: true,
|
||||
}));
|
||||
},
|
||||
formValues: {},
|
||||
setFormValues: (formValues: Record<string, any>) => {
|
||||
set({ formValues });
|
||||
},
|
||||
org: null,
|
||||
setOrg: (org: string | null | undefined) => {
|
||||
set({ org });
|
||||
},
|
||||
isPlatform: false,
|
||||
allowUpdatingUrlParams: true,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Default global store instance for backward compatibility
|
||||
*/
|
||||
export const useBookerStore = createBookerStore();
|
||||
|
||||
export const useInitializeBookerStore = ({
|
||||
username,
|
||||
|
||||
@@ -98,6 +98,7 @@ export interface BookerProps {
|
||||
hashedLink?: string | null;
|
||||
isInstantMeeting?: boolean;
|
||||
teamMemberEmail?: string | null;
|
||||
showNoAvailabilityDialog?: boolean;
|
||||
crmOwnerRecordType?: string | null;
|
||||
crmAppSlug?: string | null;
|
||||
crmRecordId?: string | null;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { useSchedule } from "@calcom/features/schedules/lib/use-schedule/useSchedule";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
import { useBookerTime } from "../components/hooks/useBookerTime";
|
||||
import { useBookerStore } from "../store";
|
||||
|
||||
export type useEventReturnType = ReturnType<typeof useEvent>;
|
||||
export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEvent>;
|
||||
@@ -19,7 +19,7 @@ export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEven
|
||||
* of combining multiple conditional hooks.
|
||||
*/
|
||||
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],
|
||||
shallow
|
||||
);
|
||||
@@ -89,7 +89,7 @@ export const useScheduleForEvent = ({
|
||||
useApiV2?: boolean;
|
||||
} = {}) => {
|
||||
const { timezone } = useBookerTime();
|
||||
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStore(
|
||||
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStoreContext(
|
||||
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
|
||||
shallow
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useMemo } from "react";
|
||||
|
||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { OutOfOfficeInSlots } from "@calcom/features/bookings/Booker/components/OutOfOfficeInSlots";
|
||||
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
|
||||
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 { useBookerTime } from "../Booker/components/hooks/useBookerTime";
|
||||
import { useBookerStore } from "../Booker/store";
|
||||
import { getQueryParam } from "../Booker/utils/query-param";
|
||||
import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay";
|
||||
import type { Slots } from "../types";
|
||||
@@ -112,8 +112,8 @@ const SlotItem = ({
|
||||
getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault");
|
||||
|
||||
const { timeFormat, timezone } = useBookerTime();
|
||||
const bookingData = useBookerStore((state) => state.bookingData);
|
||||
const layout = useBookerStore((state) => state.layout);
|
||||
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||
const layout = useBookerStoreContext((state) => state.layout);
|
||||
const hasTimeSlots = !!seatsPerTimeSlot;
|
||||
const computedDateWithUsersTimezone = dayjs.utc(slot.time).tz(timezone);
|
||||
|
||||
@@ -127,7 +127,7 @@ const SlotItem = ({
|
||||
|
||||
const offset = (usersTimezoneDate.utcOffset() - nowDate.utcOffset()) / 60;
|
||||
|
||||
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
|
||||
const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
|
||||
|
||||
const { isOverlapping, overlappingTimeEnd, overlappingTimeStart } = useCheckOverlapWithOverlay({
|
||||
start: computedDateWithUsersTimezone,
|
||||
|
||||
@@ -2,12 +2,12 @@ import { shallow } from "zustand/shallow";
|
||||
|
||||
import type { 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 { nameOfDay } from "@calcom/lib/weekday";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
|
||||
import { useBookerStore } from "../Booker/store";
|
||||
import { TimeFormatToggle } from "./TimeFormatToggle";
|
||||
|
||||
type AvailableTimesHeaderProps = {
|
||||
@@ -28,7 +28,7 @@ export const AvailableTimesHeader = ({
|
||||
customClassNames,
|
||||
}: AvailableTimesHeaderProps) => {
|
||||
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 isMonthView = layout === BookerLayouts.MONTH_VIEW;
|
||||
const isToday = dayjs().isSame(date, "day");
|
||||
|
||||
@@ -5,9 +5,9 @@ import { SystemField } from "@calcom/features/bookings/lib/SystemField";
|
||||
import type { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import { contructEmailFromPhoneNumber } from "@calcom/lib/contructEmailFromPhoneNumber";
|
||||
import { getBookingWithResponses } from "@calcom/lib/getBooking";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
export const getCalEventResponses = ({
|
||||
bookingFields,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { shallow } from "zustand/shallow";
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
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 { daysInMonth, yyyymmdd } from "@calcom/lib/dayjs";
|
||||
import type { IFromUser, IToUser } from "@calcom/lib/getUserAvailability";
|
||||
@@ -59,6 +59,8 @@ export type DatePickerProps = {
|
||||
periodData?: PeriodData;
|
||||
// Whether this is a compact sidebar view or main monthly view
|
||||
isCompact?: boolean;
|
||||
// Whether to show the no availability dialog
|
||||
showNoAvailabilityDialog?: boolean;
|
||||
};
|
||||
|
||||
const Day = ({
|
||||
@@ -164,6 +166,7 @@ const Days = ({
|
||||
isBookingInPast,
|
||||
periodData,
|
||||
isCompact,
|
||||
showNoAvailabilityDialog = true,
|
||||
...props
|
||||
}: Omit<DatePickerProps, "locale" | "className" | "weekStart"> & {
|
||||
DayComponent?: React.FC<React.ComponentProps<typeof Day>>;
|
||||
@@ -224,7 +227,7 @@ const Days = ({
|
||||
}
|
||||
}
|
||||
|
||||
const [selectedDatesAndTimes] = useBookerStore((state) => [state.selectedDatesAndTimes], shallow);
|
||||
const [selectedDatesAndTimes] = useBookerStoreContext((state) => [state.selectedDatesAndTimes], shallow);
|
||||
|
||||
const isActive = (day: dayjs.Dayjs) => {
|
||||
// for selecting a range of dates
|
||||
@@ -345,14 +348,18 @@ const Days = ({
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!props.isLoading && !isBookingInPast && includedDates && includedDates?.length === 0 && (
|
||||
<NoAvailabilityDialog
|
||||
month={month}
|
||||
nextMonthButton={nextMonthButton}
|
||||
browsingDate={browsingDate}
|
||||
periodData={periodData}
|
||||
/>
|
||||
)}
|
||||
{!props.isLoading &&
|
||||
!isBookingInPast &&
|
||||
includedDates &&
|
||||
includedDates?.length === 0 &&
|
||||
showNoAvailabilityDialog && (
|
||||
<NoAvailabilityDialog
|
||||
month={month}
|
||||
nextMonthButton={nextMonthButton}
|
||||
browsingDate={browsingDate}
|
||||
periodData={periodData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -374,6 +381,7 @@ const DatePicker = ({
|
||||
periodType: "UNLIMITED",
|
||||
},
|
||||
isCompact,
|
||||
showNoAvailabilityDialog,
|
||||
...passThroughProps
|
||||
}: DatePickerProps &
|
||||
Partial<React.ComponentProps<typeof Days>> & {
|
||||
@@ -392,7 +400,7 @@ const DatePicker = ({
|
||||
minDate && rawBrowsingDate.valueOf() < minDate.valueOf() ? dayjs(minDate) : rawBrowsingDate;
|
||||
|
||||
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 changeMonth = (newMonth: number) => {
|
||||
if (onMonthChange) {
|
||||
@@ -484,6 +492,7 @@ const DatePicker = ({
|
||||
isBookingInPast={isBookingInPast}
|
||||
periodData={periodData}
|
||||
isCompact={isCompact}
|
||||
showNoAvailabilityDialog={showNoAvailabilityDialog}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from "react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { PeriodType } from "@calcom/prisma/enums";
|
||||
|
||||
import { DatePicker } from "../DatePicker";
|
||||
@@ -16,20 +17,22 @@ describe("Tests for DatePicker Component", () => {
|
||||
test("Should render correctly with default date", async () => {
|
||||
const testDate = dayjs("2024-02-20");
|
||||
const { getByTestId } = render(
|
||||
<TooltipProvider>
|
||||
<DatePicker
|
||||
onChange={noop}
|
||||
browsingDate={testDate}
|
||||
locale="en"
|
||||
periodData={{
|
||||
periodType: PeriodType.UNLIMITED,
|
||||
periodDays: null,
|
||||
periodCountCalendarDays: false,
|
||||
periodStartDate: null,
|
||||
periodEndDate: null,
|
||||
}}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
<BookerStoreProvider>
|
||||
<TooltipProvider>
|
||||
<DatePicker
|
||||
onChange={noop}
|
||||
browsingDate={testDate}
|
||||
locale="en"
|
||||
periodData={{
|
||||
periodType: PeriodType.UNLIMITED,
|
||||
periodDays: null,
|
||||
periodCountCalendarDays: false,
|
||||
periodStartDate: null,
|
||||
periodEndDate: null,
|
||||
}}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</BookerStoreProvider>
|
||||
);
|
||||
|
||||
const selectedMonthLabel = getByTestId("selected-month-label");
|
||||
@@ -40,9 +43,11 @@ describe("Tests for DatePicker Component", () => {
|
||||
const testDate = dayjs("2024-02-20");
|
||||
const minDate = dayjs("2025-02-10");
|
||||
const { getByTestId } = render(
|
||||
<TooltipProvider>
|
||||
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
|
||||
</TooltipProvider>
|
||||
<BookerStoreProvider>
|
||||
<TooltipProvider>
|
||||
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
|
||||
</TooltipProvider>
|
||||
</BookerStoreProvider>
|
||||
);
|
||||
|
||||
const selectedMonthLabel = getByTestId("selected-month-label");
|
||||
@@ -53,9 +58,11 @@ describe("Tests for DatePicker Component", () => {
|
||||
const testDate = dayjs("2025-03-20");
|
||||
const minDate = dayjs("2025-02-10");
|
||||
const { getByTestId } = render(
|
||||
<TooltipProvider>
|
||||
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
|
||||
</TooltipProvider>
|
||||
<BookerStoreProvider>
|
||||
<TooltipProvider>
|
||||
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
|
||||
</TooltipProvider>
|
||||
</BookerStoreProvider>
|
||||
);
|
||||
|
||||
const selectedMonthLabel = getByTestId("selected-month-label");
|
||||
@@ -85,22 +92,24 @@ describe("Tests for DatePicker Component", () => {
|
||||
]);
|
||||
|
||||
const { getAllByTestId } = render(
|
||||
<TooltipProvider>
|
||||
<DatePicker
|
||||
onChange={noop}
|
||||
browsingDate={earlyMonthDate}
|
||||
locale="en"
|
||||
slots={slots}
|
||||
isCompact={false}
|
||||
periodData={{
|
||||
periodType: PeriodType.UNLIMITED,
|
||||
periodDays: null,
|
||||
periodCountCalendarDays: false,
|
||||
periodStartDate: null,
|
||||
periodEndDate: null,
|
||||
}}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
<BookerStoreProvider>
|
||||
<TooltipProvider>
|
||||
<DatePicker
|
||||
onChange={noop}
|
||||
browsingDate={earlyMonthDate}
|
||||
locale="en"
|
||||
slots={slots}
|
||||
isCompact={false}
|
||||
periodData={{
|
||||
periodType: PeriodType.UNLIMITED,
|
||||
periodDays: null,
|
||||
periodCountCalendarDays: false,
|
||||
periodStartDate: null,
|
||||
periodEndDate: null,
|
||||
}}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</BookerStoreProvider>
|
||||
);
|
||||
|
||||
const dayElements = getAllByTestId("day");
|
||||
@@ -127,22 +136,24 @@ describe("Tests for DatePicker Component", () => {
|
||||
]);
|
||||
|
||||
const { getAllByTestId, queryByText } = render(
|
||||
<TooltipProvider>
|
||||
<DatePicker
|
||||
onChange={noop}
|
||||
browsingDate={lateMonthDate}
|
||||
locale="en"
|
||||
slots={slots}
|
||||
isCompact={false}
|
||||
periodData={{
|
||||
periodType: PeriodType.UNLIMITED,
|
||||
periodDays: null,
|
||||
periodCountCalendarDays: false,
|
||||
periodStartDate: null,
|
||||
periodEndDate: null,
|
||||
}}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
<BookerStoreProvider>
|
||||
<TooltipProvider>
|
||||
<DatePicker
|
||||
onChange={noop}
|
||||
browsingDate={lateMonthDate}
|
||||
locale="en"
|
||||
slots={slots}
|
||||
isCompact={false}
|
||||
periodData={{
|
||||
periodType: PeriodType.UNLIMITED,
|
||||
periodDays: null,
|
||||
periodCountCalendarDays: false,
|
||||
periodStartDate: null,
|
||||
periodEndDate: null,
|
||||
}}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</BookerStoreProvider>
|
||||
);
|
||||
|
||||
const dayElements = getAllByTestId("day");
|
||||
@@ -165,22 +176,24 @@ describe("Tests for DatePicker Component", () => {
|
||||
const slots = createMockSlots(["2024-01-25", "2024-02-01"]);
|
||||
|
||||
const { getAllByTestId } = render(
|
||||
<TooltipProvider>
|
||||
<DatePicker
|
||||
onChange={noop}
|
||||
browsingDate={lateMonthDate}
|
||||
locale="en"
|
||||
slots={slots}
|
||||
isCompact={true} // This should force traditional view
|
||||
periodData={{
|
||||
periodType: PeriodType.UNLIMITED,
|
||||
periodDays: null,
|
||||
periodCountCalendarDays: false,
|
||||
periodStartDate: null,
|
||||
periodEndDate: null,
|
||||
}}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
<BookerStoreProvider>
|
||||
<TooltipProvider>
|
||||
<DatePicker
|
||||
onChange={noop}
|
||||
browsingDate={lateMonthDate}
|
||||
locale="en"
|
||||
slots={slots}
|
||||
isCompact={true} // This should force traditional view
|
||||
periodData={{
|
||||
periodType: PeriodType.UNLIMITED,
|
||||
periodDays: null,
|
||||
periodCountCalendarDays: false,
|
||||
periodStartDate: null,
|
||||
periodEndDate: null,
|
||||
}}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</BookerStoreProvider>
|
||||
);
|
||||
|
||||
const dayElements = getAllByTestId("day");
|
||||
|
||||
@@ -43,7 +43,7 @@ export const TeamEventTypeForm = ({
|
||||
|
||||
return (
|
||||
<Form form={form} handleSubmit={handleSubmit}>
|
||||
<div className="mt-3 space-y-6 pb-11">
|
||||
<div className="mt-1 space-y-6">
|
||||
<TextField
|
||||
type="hidden"
|
||||
labelProps={{ style: { display: "none" } }}
|
||||
|
||||
@@ -11,7 +11,12 @@ import { shallow } from "zustand/shallow";
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
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 DatePicker from "@calcom/features/calendars/DatePicker";
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
@@ -260,13 +265,21 @@ const EmailEmbed = ({
|
||||
org: orgSlug,
|
||||
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],
|
||||
shallow
|
||||
);
|
||||
const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot, setTimezone] =
|
||||
useBookerStore(
|
||||
useBookerStoreContext(
|
||||
(state) => [
|
||||
state.setSelectedDate,
|
||||
state.setMonth,
|
||||
@@ -686,7 +699,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
const emailContentRef = useRef<HTMLDivElement>(null);
|
||||
const { data } = useSession();
|
||||
|
||||
const [month, selectedDatesAndTimes] = useBookerStore(
|
||||
const [month, selectedDatesAndTimes] = useBookerStoreContext(
|
||||
(state) => [state.month, state.selectedDatesAndTimes],
|
||||
shallow
|
||||
);
|
||||
@@ -1369,32 +1382,34 @@ export const EmbedDialog = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...(noQueryParamMode
|
||||
? {
|
||||
open: embedState !== null,
|
||||
onOpenChange: (open) => !open && handleDialogClose(),
|
||||
}
|
||||
: {
|
||||
// Must not set name when noQueryParam mode as required by Dialog component
|
||||
name: "embed",
|
||||
clearQueryParamsOnClose: queryParamsForDialog,
|
||||
})}>
|
||||
{!embedParams.embedType ? (
|
||||
<ChooseEmbedTypesDialogContent types={types} noQueryParamMode={noQueryParamMode} />
|
||||
) : (
|
||||
<EmbedTypeCodeAndPreviewDialogContent
|
||||
embedType={embedParams.embedType as EmbedType}
|
||||
embedUrl={embedParams.embedUrl}
|
||||
namespace={embedParams.namespace}
|
||||
tabs={tabs}
|
||||
types={types}
|
||||
eventTypeHideOptionDisabled={eventTypeHideOptionDisabled}
|
||||
defaultBrandColor={defaultBrandColor}
|
||||
noQueryParamMode={noQueryParamMode}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
<BookerStoreProvider>
|
||||
<Dialog
|
||||
{...(noQueryParamMode
|
||||
? {
|
||||
open: embedState !== null,
|
||||
onOpenChange: (open) => !open && handleDialogClose(),
|
||||
}
|
||||
: {
|
||||
// Must not set name when noQueryParam mode as required by Dialog component
|
||||
name: "embed",
|
||||
clearQueryParamsOnClose: queryParamsForDialog,
|
||||
})}>
|
||||
{!embedParams.embedType ? (
|
||||
<ChooseEmbedTypesDialogContent types={types} noQueryParamMode={noQueryParamMode} />
|
||||
) : (
|
||||
<EmbedTypeCodeAndPreviewDialogContent
|
||||
embedType={embedParams.embedType as EmbedType}
|
||||
embedUrl={embedParams.embedUrl}
|
||||
namespace={embedParams.namespace}
|
||||
tabs={tabs}
|
||||
types={types}
|
||||
eventTypeHideOptionDisabled={eventTypeHideOptionDisabled}
|
||||
defaultBrandColor={defaultBrandColor}
|
||||
noQueryParamMode={noQueryParamMode}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
</BookerStoreProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, Suspense } from "react";
|
||||
import { useState, Suspense, useMemo } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { z } from "zod";
|
||||
@@ -10,9 +10,10 @@ import {
|
||||
SelectedCalendarSettingsScope,
|
||||
SelectedCalendarsSettingsWebWrapperSkeleton,
|
||||
} from "@calcom/atoms/selected-calendars/wrappers/SelectedCalendarsSettingsWebWrapper";
|
||||
import { Timezone as PlatformTimzoneSelect } from "@calcom/atoms/timezone";
|
||||
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
|
||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
import { TimezoneSelect } from "@calcom/features/components/timezone-select";
|
||||
import { TimezoneSelect as WebTimezoneSelect } from "@calcom/features/components/timezone-select";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import {
|
||||
allowDisablingAttendeeConfirmationEmails,
|
||||
@@ -565,6 +566,10 @@ export const EventAdvancedTab = ({
|
||||
userEmail = removePlatformClientIdFromEmail(userEmail, platformContext.clientId);
|
||||
}
|
||||
|
||||
const TimezoneSelect = useMemo(() => {
|
||||
return isPlatform ? PlatformTimzoneSelect : WebTimezoneSelect;
|
||||
}, [isPlatform]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<calendarComponents.CalendarSettings
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
|
||||
@@ -63,5 +64,9 @@ const TroubleshooterComponent = ({ month }: TroubleshooterProps) => {
|
||||
};
|
||||
|
||||
export const Troubleshooter = ({ month }: TroubleshooterProps) => {
|
||||
return <TroubleshooterComponent month={month} />;
|
||||
return (
|
||||
<BookerStoreProvider>
|
||||
<TroubleshooterComponent month={month} />
|
||||
</BookerStoreProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -327,7 +327,9 @@ export default class EventManager {
|
||||
}
|
||||
|
||||
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
|
||||
const isMSTeamsWithOutlookCalendar = evt.location === MSTeamsLocationType && mainHostDestinationCalendar?.integration === "office365_calendar";
|
||||
const isMSTeamsWithOutlookCalendar =
|
||||
evt.location === MSTeamsLocationType &&
|
||||
mainHostDestinationCalendar?.integration === "office365_calendar";
|
||||
|
||||
const results: Array<EventResult<Exclude<Event, AdditionalInformation>>> = [];
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ export async function getConnectedApps({
|
||||
if (includeTeamInstalledApps || teamId) {
|
||||
const teamsQuery = await prisma.team.findMany({
|
||||
where: {
|
||||
...(teamId ? { id: teamId } : {}),
|
||||
members: {
|
||||
some: {
|
||||
userId: user.id,
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function loadTranslations(_locale: string, _ns: string) {
|
||||
const response = await fetchWithTimeout(
|
||||
url,
|
||||
{
|
||||
cache: process.env.NODE_ENV === "production" ? "force-cache" : "no-store",
|
||||
cache: "no-store",
|
||||
},
|
||||
process.env.NODE_ENV === "development" ? 30000 : 3000
|
||||
);
|
||||
|
||||
@@ -229,4 +229,61 @@ export class CredentialRepository {
|
||||
static async updateWhereId({ id, data }: { id: number; data: { key: Prisma.InputJsonValue } }) {
|
||||
return prisma.credential.update({ where: { id }, data });
|
||||
}
|
||||
|
||||
static async findPaymentCredentialByAppIdAndTeamId({
|
||||
appId,
|
||||
teamId,
|
||||
}: {
|
||||
appId: string | null;
|
||||
teamId: number;
|
||||
}) {
|
||||
return await prisma.credential.findFirst({
|
||||
where: {
|
||||
teamId,
|
||||
appId,
|
||||
},
|
||||
include: {
|
||||
app: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async findPaymentCredentialByAppIdAndUserId({
|
||||
appId,
|
||||
userId,
|
||||
}: {
|
||||
appId: string | null;
|
||||
userId: number;
|
||||
}) {
|
||||
return await prisma.credential.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
appId,
|
||||
},
|
||||
include: {
|
||||
app: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async findPaymentCredentialByAppIdAndUserIdOrTeamId({
|
||||
appId,
|
||||
userId,
|
||||
teamId,
|
||||
}: {
|
||||
appId: string | null;
|
||||
userId: number;
|
||||
teamId?: number | null;
|
||||
}) {
|
||||
const idToSearchObject = teamId ? { teamId } : { userId };
|
||||
return await prisma.credential.findFirst({
|
||||
where: {
|
||||
...idToSearchObject,
|
||||
appId,
|
||||
},
|
||||
include: {
|
||||
app: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,4 +333,21 @@ export class TeamRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findParentOrganizationByTeamId(teamId: number) {
|
||||
const team = await this.prismaClient.team.findUnique({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
select: {
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return team?.parent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
## 1.1.2
|
||||
|
||||
## 1.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#22925](https://github.com/calcom/cal.com/pull/22925) [`4c01f17`](https://github.com/calcom/cal.com/commit/4c01f171aebcc5a64453c27e950303902cc15309) Thanks [@sean-brydon](https://github.com/sean-brydon)! - Fixes an issues for the Booker atom wherein when multiple widgets were being placed on the same page, changes made in one widget would also get reflected in the others.
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#23060](https://github.com/calcom/cal.com/pull/23060) [`293dd1a`](https://github.com/calcom/cal.com/commit/293dd1a1402fbc0748dffd5619a1b1aacaea9d60) Thanks [@SomayChauhan](https://github.com/SomayChauhan)! - fix: EventTypeSettings Atom crashes on enabling `Lock timezone on booking` in advanced tab
|
||||
|
||||
- [#22911](https://github.com/calcom/cal.com/pull/22911) [`d3fbc73`](https://github.com/calcom/cal.com/commit/d3fbc73a3962a9bcca6a95870b146a97f4c7d996) Thanks [@SomayChauhan](https://github.com/SomayChauhan)! - Added new callback functions to the handleFormSubmit method in the EventTypeSettings and AvailabilitySettings atoms. The handleFormSubmit method now accepts an optional callbacks object with the following properties:
|
||||
|
||||
- **onSuccess**: Called when the form submission is successful, allowing additional logic to be executed after the update.
|
||||
|
||||
- **onError**: Called when an error occurs during form submission, providing details about the error to handle specific cases or display custom messages.
|
||||
|
||||
- [#23052](https://github.com/calcom/cal.com/pull/23052) [`fc2e81e`](https://github.com/calcom/cal.com/commit/fc2e81e89a6e320c7346dad3259dd51ed700fb30) Thanks [@SomayChauhan](https://github.com/SomayChauhan)! - Added showNoAvailabilityDialog prop to the Booker atom – a Boolean that controls whether the 'no availability' dialog is displayed
|
||||
|
||||
- [#22976](https://github.com/calcom/cal.com/pull/22976) [`abe92cd`](https://github.com/calcom/cal.com/commit/abe92cd693f33d20113f5357a960baec042e5256) Thanks [@supalarry](https://github.com/supalarry)! - feat: style calendar settings and availability overrides
|
||||
|
||||
- [#22956](https://github.com/calcom/cal.com/pull/22956) [`d18e233`](https://github.com/calcom/cal.com/commit/d18e23378b699e734e9dbbdf93225b02a9059ff4) Thanks [@supalarry](https://github.com/supalarry)! - booker atom: allow toggling org and team info when booking round robin
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -13,6 +13,7 @@ import React, {
|
||||
import { Controller, useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import { TimezoneSelect as WebTimezoneSelect } from "@calcom/features/components/timezone-select";
|
||||
import type {
|
||||
@@ -672,19 +673,21 @@ export const AvailabilitySettings = forwardRef<AvailabilitySettingsFormRef, Avai
|
||||
</div>
|
||||
</div>
|
||||
{enableOverrides && (
|
||||
<DateOverride
|
||||
workingHours={schedule.workingHours}
|
||||
userTimeFormat={timeFormat}
|
||||
handleSubmit={handleSubmit}
|
||||
travelSchedules={travelSchedules}
|
||||
weekStart={
|
||||
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
||||
weekStart
|
||||
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
}
|
||||
overridesModalClassNames={customClassNames?.overridesModalClassNames}
|
||||
classNames={customClassNames?.dateOverrideClassNames}
|
||||
/>
|
||||
<BookerStoreProvider>
|
||||
<DateOverride
|
||||
workingHours={schedule.workingHours}
|
||||
userTimeFormat={timeFormat}
|
||||
handleSubmit={handleSubmit}
|
||||
travelSchedules={travelSchedules}
|
||||
weekStart={
|
||||
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
||||
weekStart
|
||||
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
}
|
||||
overridesModalClassNames={customClassNames?.overridesModalClassNames}
|
||||
classNames={customClassNames?.dateOverrideClassNames}
|
||||
/>
|
||||
</BookerStoreProvider>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-40 col-span-3 hidden space-y-2 md:block lg:col-span-1">
|
||||
|
||||
@@ -6,6 +6,11 @@ import { shallow } from "zustand/shallow";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { Booker as BookerComponent } from "@calcom/features/bookings/Booker";
|
||||
import {
|
||||
BookerStoreProvider,
|
||||
useInitializeBookerStoreContext,
|
||||
useBookerStoreContext,
|
||||
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
|
||||
import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
|
||||
import { useLocalSet } from "@calcom/features/bookings/Booker/components/hooks/useLocalSet";
|
||||
@@ -40,7 +45,7 @@ import type {
|
||||
BookerStoreValues,
|
||||
} from "./types";
|
||||
|
||||
export const BookerPlatformWrapper = (
|
||||
const BookerPlatformWrapperComponent = (
|
||||
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
|
||||
) => {
|
||||
const {
|
||||
@@ -58,20 +63,24 @@ export const BookerPlatformWrapper = (
|
||||
handleSlotReservation,
|
||||
onTimeslotsLoaded,
|
||||
startTime: customStartTime,
|
||||
showNoAvailabilityDialog,
|
||||
} = props;
|
||||
const layout = BookerLayouts[view];
|
||||
|
||||
const { clientId } = useAtomsContext();
|
||||
const teamId: number | undefined = props.isTeamEvent ? props.teamId : undefined;
|
||||
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
|
||||
const setSelectedDate = useBookerStore((state) => state.setSelectedDate);
|
||||
const setSelectedDuration = useBookerStore((state) => state.setSelectedDuration);
|
||||
const setBookingData = useBookerStore((state) => state.setBookingData);
|
||||
const setOrg = useBookerStore((state) => state.setOrg);
|
||||
const bookingData = useBookerStore((state) => state.bookingData);
|
||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
||||
const setSelectedMonth = useBookerStore((state) => state.setMonth);
|
||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
||||
const [bookerState, setBookerState] = useBookerStoreContext(
|
||||
(state) => [state.state, state.setState],
|
||||
shallow
|
||||
);
|
||||
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
|
||||
const setSelectedDuration = useBookerStoreContext((state) => state.setSelectedDuration);
|
||||
const setBookingData = useBookerStoreContext((state) => state.setBookingData);
|
||||
const setOrg = useBookerStoreContext((state) => state.setOrg);
|
||||
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||
const setSelectedTimeslot = useBookerStoreContext((state) => state.setSelectedTimeslot);
|
||||
const setSelectedMonth = useBookerStoreContext((state) => state.setMonth);
|
||||
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||
|
||||
const [isOverlayCalendarEnabled, setIsOverlayCalendarEnabled] = useState(
|
||||
Boolean(localStorage?.getItem?.("overlayCalendarSwitchDefault"))
|
||||
@@ -170,11 +179,27 @@ export const BookerPlatformWrapper = (
|
||||
isPlatform: true,
|
||||
allowUpdatingUrlParams,
|
||||
});
|
||||
const [dayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
|
||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||
useInitializeBookerStoreContext({
|
||||
...props,
|
||||
teamMemberEmail,
|
||||
crmAppSlug,
|
||||
crmOwnerRecordType,
|
||||
crmRecordId: props.crmRecordId,
|
||||
eventId: event?.data?.id,
|
||||
rescheduleUid: props.rescheduleUid ?? null,
|
||||
bookingUid: props.bookingUid ?? null,
|
||||
layout: layout,
|
||||
org: props.entity?.orgSlug,
|
||||
username,
|
||||
bookingData,
|
||||
isPlatform: true,
|
||||
allowUpdatingUrlParams,
|
||||
});
|
||||
const [dayCount] = useBookerStoreContext((state) => [state.dayCount, state.setDayCount], shallow);
|
||||
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
|
||||
|
||||
const month = useBookerStore((state) => state.month);
|
||||
const eventSlug = useBookerStore((state) => state.eventSlug);
|
||||
const month = useBookerStoreContext((state) => state.month);
|
||||
const eventSlug = useBookerStoreContext((state) => state.eventSlug);
|
||||
|
||||
const { data: session } = useMe();
|
||||
const hasSession = !!session;
|
||||
@@ -465,6 +490,7 @@ export const BookerPlatformWrapper = (
|
||||
customClassNames={props.customClassNames}
|
||||
eventSlug={props.eventSlug}
|
||||
username={username}
|
||||
showNoAvailabilityDialog={showNoAvailabilityDialog}
|
||||
entity={
|
||||
event?.data?.entity ?? {
|
||||
considerUnpublished: false,
|
||||
@@ -555,6 +581,16 @@ export const BookerPlatformWrapper = (
|
||||
);
|
||||
};
|
||||
|
||||
export const BookerPlatformWrapper = (
|
||||
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
|
||||
) => {
|
||||
return (
|
||||
<BookerStoreProvider>
|
||||
<BookerPlatformWrapperComponent {...props} />
|
||||
</BookerStoreProvider>
|
||||
);
|
||||
};
|
||||
|
||||
function formatUsername(username: string | string[]): string {
|
||||
if (typeof username === "string") {
|
||||
return username;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useSession } from "next-auth/react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useMemo, useCallback, useEffect } from "react";
|
||||
import React from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
@@ -11,6 +12,11 @@ import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
|
||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import type { BookerProps } from "@calcom/features/bookings/Booker";
|
||||
import { Booker as BookerComponent } from "@calcom/features/bookings/Booker";
|
||||
import {
|
||||
BookerStoreProvider,
|
||||
useInitializeBookerStoreContext,
|
||||
useBookerStoreContext,
|
||||
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
|
||||
import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
|
||||
import { useBookings } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
|
||||
@@ -18,19 +24,20 @@ import { useCalendars } from "@calcom/features/bookings/Booker/components/hooks/
|
||||
import { useSlots } from "@calcom/features/bookings/Booker/components/hooks/useSlots";
|
||||
import { useVerifyCode } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode";
|
||||
import { useVerifyEmail } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail";
|
||||
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
|
||||
import { useBrandColors } from "@calcom/features/bookings/Booker/utils/use-brand-colors";
|
||||
import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||
import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export type BookerWebWrapperAtomProps = BookerProps & {
|
||||
eventData?: NonNullable<Awaited<ReturnType<typeof getPublicEvent>>>;
|
||||
};
|
||||
|
||||
export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
|
||||
const BookerPlatformWrapperComponent = (props: BookerWebWrapperAtomProps) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -74,10 +81,20 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
|
||||
org: props.entity.orgSlug,
|
||||
timezone,
|
||||
});
|
||||
useInitializeBookerStoreContext({
|
||||
...props,
|
||||
eventId: props.entity.eventTypeId ?? event?.data?.id,
|
||||
rescheduleUid,
|
||||
rescheduledBy,
|
||||
bookingUid: bookingUid,
|
||||
layout: bookerLayout.isMobile ? "mobile" : bookerLayout.defaultLayout,
|
||||
org: props.entity.orgSlug,
|
||||
timezone,
|
||||
});
|
||||
|
||||
const [bookerState, _] = useBookerStore((state) => [state.state, state.setState], shallow);
|
||||
const [dayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
|
||||
const [month] = useBookerStore((state) => [state.month, state.setMonth], shallow);
|
||||
const [bookerState, _] = useBookerStoreContext((state) => [state.state, state.setState], shallow);
|
||||
const [dayCount] = useBookerStoreContext((state) => [state.dayCount, state.setDayCount], shallow);
|
||||
const [month] = useBookerStoreContext((state) => [state.month, state.setMonth], shallow);
|
||||
|
||||
const { data: session } = useSession();
|
||||
const routerQuery = useRouterQuery();
|
||||
@@ -263,3 +280,11 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
|
||||
return (
|
||||
<BookerStoreProvider>
|
||||
<BookerPlatformWrapperComponent {...props} />
|
||||
</BookerStoreProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -150,7 +150,7 @@ const EventType = forwardRef<
|
||||
const description = message ? t(message) : t(err.message);
|
||||
toast({ description });
|
||||
onError?.(currentValues, err);
|
||||
|
||||
|
||||
const errorObj = new Error(description);
|
||||
callbacksRef.current?.onError?.(errorObj);
|
||||
},
|
||||
@@ -175,24 +175,27 @@ const EventType = forwardRef<
|
||||
|
||||
const callbacksRef = useRef<{ onSuccess?: () => void; onError?: (error: Error) => void }>({});
|
||||
|
||||
const handleFormSubmit = useCallback((customCallbacks?: { onSuccess?: () => void; onError?: (error: Error) => void }) => {
|
||||
if (customCallbacks) {
|
||||
callbacksRef.current = customCallbacks;
|
||||
}
|
||||
const handleFormSubmit = useCallback(
|
||||
(customCallbacks?: { onSuccess?: () => void; onError?: (error: Error) => void }) => {
|
||||
if (customCallbacks) {
|
||||
callbacksRef.current = customCallbacks;
|
||||
}
|
||||
|
||||
if (saveButtonRef.current) {
|
||||
saveButtonRef.current.click();
|
||||
} else {
|
||||
form.handleSubmit((data) => {
|
||||
try {
|
||||
handleSubmit(data);
|
||||
customCallbacks?.onSuccess?.();
|
||||
} catch (error) {
|
||||
customCallbacks?.onError?.(error as Error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [handleSubmit, form]);
|
||||
if (saveButtonRef.current) {
|
||||
saveButtonRef.current.click();
|
||||
} else {
|
||||
form.handleSubmit((data) => {
|
||||
try {
|
||||
handleSubmit(data);
|
||||
customCallbacks?.onSuccess?.();
|
||||
} catch (error) {
|
||||
customCallbacks?.onError?.(error as Error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
},
|
||||
[handleSubmit, form]
|
||||
);
|
||||
|
||||
const validateForm = useCallback(async () => {
|
||||
const isValid = await form.trigger();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { useBookerTime } from "@calcom/features/bookings/Booker/components/hooks/useBookerTime";
|
||||
import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
|
||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import { mapBookingToMutationInput, mapRecurringBookingToMutationInput } from "@calcom/features/bookings/lib";
|
||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -46,23 +46,23 @@ export const useHandleBookEvent = ({
|
||||
isBookingDryRun,
|
||||
}: UseHandleBookingProps) => {
|
||||
const isPlatform = useIsPlatform();
|
||||
const setFormValues = useBookerStore((state) => state.setFormValues);
|
||||
const storeTimeSlot = useBookerStore((state) => state.selectedTimeslot);
|
||||
const duration = useBookerStore((state) => state.selectedDuration);
|
||||
const setFormValues = useBookerStoreContext((state) => state.setFormValues);
|
||||
const storeTimeSlot = useBookerStoreContext((state) => state.selectedTimeslot);
|
||||
const duration = useBookerStoreContext((state) => state.selectedDuration);
|
||||
const { timezone } = useBookerTime();
|
||||
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
||||
const rescheduledBy = useBookerStore((state) => state.rescheduledBy);
|
||||
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
|
||||
const rescheduledBy = useBookerStoreContext((state) => state.rescheduledBy);
|
||||
const { t, i18n } = useLocale();
|
||||
const username = useBookerStore((state) => state.username);
|
||||
const recurringEventCount = useBookerStore((state) => state.recurringEventCount);
|
||||
const bookingData = useBookerStore((state) => state.bookingData);
|
||||
const seatedEventData = useBookerStore((state) => state.seatedEventData);
|
||||
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
|
||||
const orgSlug = useBookerStore((state) => state.org);
|
||||
const teamMemberEmail = useBookerStore((state) => state.teamMemberEmail);
|
||||
const crmOwnerRecordType = useBookerStore((state) => state.crmOwnerRecordType);
|
||||
const crmAppSlug = useBookerStore((state) => state.crmAppSlug);
|
||||
const crmRecordId = useBookerStore((state) => state.crmRecordId);
|
||||
const username = useBookerStoreContext((state) => state.username);
|
||||
const recurringEventCount = useBookerStoreContext((state) => state.recurringEventCount);
|
||||
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||
const seatedEventData = useBookerStoreContext((state) => state.seatedEventData);
|
||||
const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
|
||||
const orgSlug = useBookerStoreContext((state) => state.org);
|
||||
const teamMemberEmail = useBookerStoreContext((state) => state.teamMemberEmail);
|
||||
const crmOwnerRecordType = useBookerStoreContext((state) => state.crmOwnerRecordType);
|
||||
const crmAppSlug = useBookerStoreContext((state) => state.crmAppSlug);
|
||||
const crmRecordId = useBookerStoreContext((state) => state.crmRecordId);
|
||||
const handleError = (err: any) => {
|
||||
const errorMessage = err?.message ? t(err.message) : t("can_you_try_again");
|
||||
showToast(errorMessage, "error");
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
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 type { BookerEvent } from "@calcom/features/bookings/types";
|
||||
import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
|
||||
@@ -36,8 +36,8 @@ export const useSlots = (
|
||||
handleSlotReservation,
|
||||
}: UseSlotsCallbacks & { isBookingDryRun?: boolean } = {}
|
||||
) => {
|
||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
||||
const [selectedTimeslot, setSelectedTimeslot] = useBookerStore(
|
||||
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||
const [selectedTimeslot, setSelectedTimeslot] = useBookerStoreContext(
|
||||
(state) => [state.selectedTimeslot, state.setSelectedTimeslot],
|
||||
shallow
|
||||
);
|
||||
@@ -52,7 +52,7 @@ export const useSlots = (
|
||||
onError: onReserveSlotError,
|
||||
});
|
||||
|
||||
const seatedEventData = useBookerStore((state) => state.seatedEventData);
|
||||
const seatedEventData = useBookerStoreContext((state) => state.seatedEventData);
|
||||
|
||||
const removeSelectedSlot = useDeleteSelectedSlot({
|
||||
onSuccess: onDeleteSlotSuccess,
|
||||
@@ -85,7 +85,7 @@ export const useSlots = (
|
||||
}
|
||||
};
|
||||
|
||||
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
||||
const timeslot = useBookerStoreContext((state) => state.selectedTimeslot);
|
||||
|
||||
useEffect(() => {
|
||||
handleReserveSlot();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"type": "module",
|
||||
"description": "Customizable UI components to integrate scheduling into your product.",
|
||||
"authors": "Cal.com, Inc.",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.1",
|
||||
"scripts": {
|
||||
"dev": "yarn vite build --watch & npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify --watch",
|
||||
"build": "NODE_OPTIONS='--max_old_space_size=12288' rm -rf dist && yarn vite build && npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify && mkdir ./dist/packages/prisma-client && cp -rf ../../../node_modules/.prisma/client/*.d.ts ./dist/packages/prisma-client",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const DEFAULT_EXPECT_TIMEOUT = process.env.CI ? 30000 : 120000;
|
||||
const DEFAULT_TEST_TIMEOUT = process.env.CI ? 60000 : 240000;
|
||||
@@ -12,35 +12,35 @@ export default defineConfig({
|
||||
timeout: DEFAULT_TEST_TIMEOUT,
|
||||
fullyParallel: true,
|
||||
reporter: [
|
||||
['list'],
|
||||
['html', { outputFolder: './test-results/reports/playwright-html-report', open: 'never' }],
|
||||
["list"],
|
||||
["html", { outputFolder: "./test-results/reports/playwright-html-report", open: "never" }],
|
||||
],
|
||||
outputDir: './test-results/results',
|
||||
outputDir: "./test-results/results",
|
||||
use: {
|
||||
baseURL: 'http://localhost:4322',
|
||||
locale: 'en-US',
|
||||
trace: 'retain-on-failure',
|
||||
baseURL: "http://localhost:4322",
|
||||
locale: "en-US",
|
||||
trace: "retain-on-failure",
|
||||
headless,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: '@calcom/base',
|
||||
testDir: './tests',
|
||||
name: "@calcom/base",
|
||||
testDir: "./tests",
|
||||
testMatch: /.*\.e2e\.tsx?/,
|
||||
expect: {
|
||||
timeout: DEFAULT_EXPECT_TIMEOUT,
|
||||
},
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
locale: 'en-US',
|
||||
...devices["Desktop Chrome"],
|
||||
locale: "en-US",
|
||||
},
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: process.env.CI
|
||||
command: process.env.CI
|
||||
? `yarn workspace @calcom/atoms dev-on && yarn workspace @calcom/atoms build && rm -f prisma/dev.db && yarn prisma db push && NEXT_PUBLIC_IS_E2E=1 NODE_ENV=test NEXT_PUBLIC_X_CAL_ID="${process.env.ATOMS_E2E_OAUTH_CLIENT_ID}" X_CAL_SECRET_KEY="${process.env.ATOMS_E2E_OAUTH_CLIENT_SECRET}" NEXT_PUBLIC_CALCOM_API_URL="${process.env.ATOMS_E2E_API_URL}" VITE_BOOKER_EMBED_OAUTH_CLIENT_ID="${process.env.ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED}" VITE_BOOKER_EMBED_API_URL="${process.env.ATOMS_E2E_API_URL}" ORGANIZATION_ID=${process.env.ATOMS_E2E_ORG_ID} yarn dev:e2e`
|
||||
: `rm -f prisma/dev.db && yarn prisma db push && yarn dev:e2e`,
|
||||
url: 'http://localhost:4322',
|
||||
url: "http://localhost:4322",
|
||||
timeout: 600_000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
|
||||
+10
-10
@@ -1,19 +1,19 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test('create event type using CreateEventTypeAtom', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
test("create event type using CreateEventTypeAtom", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.goto('/event-types');
|
||||
await page.goto("/event-types");
|
||||
|
||||
await expect(page).toHaveURL('/event-types');
|
||||
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
await expect(page).toHaveURL("/event-types");
|
||||
|
||||
await page.fill('[data-testid="event-type-quick-chat"]', 'e2e event');
|
||||
await expect(page.locator("body")).toBeVisible();
|
||||
|
||||
await page.fill('textarea[placeholder="A quick video meeting."]', 'This is an e2e test event description');
|
||||
await page.fill('[data-testid="event-type-quick-chat"]', "e2e event");
|
||||
|
||||
await page.waitForSelector('button[type="submit"]:has-text("Continue")', { state: 'visible' });
|
||||
await page.fill('textarea[placeholder="A quick video meeting."]', "This is an e2e test event description");
|
||||
|
||||
await page.waitForSelector('button[type="submit"]:has-text("Continue")', { state: "visible" });
|
||||
await page.click('button[type="submit"]:has-text("Continue")');
|
||||
|
||||
await expect(page.locator('h1:has-text("E2e Event")')).toBeVisible();
|
||||
|
||||
@@ -18,7 +18,8 @@ export class CreatePrivateLinkInput {
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@ApiPropertyOptional({
|
||||
description: "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
||||
description:
|
||||
"Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
||||
type: Number,
|
||||
example: 10,
|
||||
minimum: 1,
|
||||
@@ -58,4 +59,25 @@ export class UpdatePrivateLinkInput {
|
||||
maxUsageCount?: number;
|
||||
}
|
||||
|
||||
export class UpdatePrivateLinkBody {
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
@ApiPropertyOptional({
|
||||
description: "New expiration date for time-based links",
|
||||
type: Date,
|
||||
example: "2024-12-31T23:59:59.000Z",
|
||||
})
|
||||
expiresAt?: Date;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@ApiPropertyOptional({
|
||||
description: "New maximum number of times the link can be used",
|
||||
type: Number,
|
||||
example: 10,
|
||||
minimum: 1,
|
||||
})
|
||||
maxUsageCount?: number;
|
||||
}
|
||||
|
||||
@@ -128,5 +128,3 @@ export class DeletePrivateLinkOutput {
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,18 @@ export async function getBookings({
|
||||
role: {
|
||||
in: ["ADMIN", "OWNER"],
|
||||
},
|
||||
...(user.orgId && {
|
||||
OR: [
|
||||
{
|
||||
teamId: user.orgId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
parentId: user.orgId,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -95,21 +95,18 @@ export const createTeamsHandler = async ({ ctx, input }: CreateTeamsOptions) =>
|
||||
teamNames.map((item) => slugify(item)).includes(slug)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
moveTeams
|
||||
.filter((team) => team.shouldMove)
|
||||
.map(async ({ id: teamId, newSlug }) => {
|
||||
await moveTeam({
|
||||
teamId,
|
||||
newSlug,
|
||||
org: {
|
||||
...organization,
|
||||
ownerId: organizationOwner.id,
|
||||
},
|
||||
creationSource,
|
||||
});
|
||||
})
|
||||
);
|
||||
// Process team migrations sequentially to avoid race conditions - Moving a team invites members to the organization again and there are known unique constraints failure in membership and profile creation if done in parallel and a user happens to be part of more than one team
|
||||
for (const team of moveTeams.filter((team) => team.shouldMove)) {
|
||||
await moveTeam({
|
||||
teamId: team.id,
|
||||
newSlug: team.newSlug,
|
||||
org: {
|
||||
...organization,
|
||||
ownerId: organizationOwner.id,
|
||||
},
|
||||
creationSource,
|
||||
});
|
||||
}
|
||||
|
||||
if (duplicatedSlugs.length === teamNames.length) {
|
||||
return { duplicatedSlugs };
|
||||
|
||||
@@ -4,6 +4,9 @@ import { sendNoShowFeeChargedEmail } from "@calcom/emails";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { ErrorWithCode } from "@calcom/lib/errors";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
||||
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
@@ -20,6 +23,7 @@ interface ChargeCardHandlerOptions {
|
||||
}
|
||||
export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions) => {
|
||||
const { prisma } = ctx;
|
||||
const teamRepository = new TeamRepository(prisma);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
@@ -84,18 +88,14 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions
|
||||
},
|
||||
};
|
||||
|
||||
const idToSearchObject = booking.eventType?.teamId
|
||||
? { teamId: booking.eventType.teamId }
|
||||
: { userId: ctx.user.id };
|
||||
const userId = ctx.user.id;
|
||||
const teamId = booking.eventType?.teamId;
|
||||
const appId = booking.payment[0].appId;
|
||||
|
||||
if (booking.eventType?.teamId) {
|
||||
const userIsInTeam = await prisma.membership.findUnique({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
userId: ctx.user.id,
|
||||
teamId: booking.eventType?.teamId,
|
||||
},
|
||||
},
|
||||
if (teamId) {
|
||||
const userIsInTeam = await MembershipRepository.findUniqueByUserIdAndTeamId({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!userIsInTeam) {
|
||||
@@ -103,16 +103,24 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions
|
||||
}
|
||||
}
|
||||
|
||||
const paymentCredential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
...idToSearchObject,
|
||||
appId: booking.payment[0].appId,
|
||||
},
|
||||
include: {
|
||||
app: true,
|
||||
},
|
||||
let paymentCredential = await CredentialRepository.findPaymentCredentialByAppIdAndUserIdOrTeamId({
|
||||
appId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!paymentCredential && teamId) {
|
||||
// See if the team event belongs to an org
|
||||
const org = await teamRepository.findParentOrganizationByTeamId(teamId);
|
||||
|
||||
if (org) {
|
||||
paymentCredential = await CredentialRepository.findPaymentCredentialByAppIdAndTeamId({
|
||||
appId,
|
||||
teamId: org.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!paymentCredential?.app) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid payment credential" });
|
||||
}
|
||||
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Team, User, Membership, Profile } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
|
||||
import inviteMemberHandler, { inviteMembersWithNoInviterPermissionCheck } from "./inviteMember.handler";
|
||||
|
||||
// Helper functions for database verification
|
||||
async function verifyProfileExists(userId: number, organizationId: number): Promise<Profile | null> {
|
||||
return await prisma.profile.findUnique({
|
||||
where: {
|
||||
userId_organizationId: {
|
||||
userId,
|
||||
organizationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function verifyMembershipExists(userId: number, teamId: number): Promise<Membership | null> {
|
||||
return await prisma.membership.findUnique({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function verifyUserOrganizationConsistency(userId: number): Promise<{
|
||||
profileCount: number;
|
||||
acceptedMembershipCount: number;
|
||||
pendingMembershipCount: number;
|
||||
}> {
|
||||
const profiles = await prisma.profile.count({ where: { userId } });
|
||||
const acceptedMemberships = await prisma.membership.count({
|
||||
where: { userId, accepted: true },
|
||||
});
|
||||
const pendingMemberships = await prisma.membership.count({
|
||||
where: { userId, accepted: false },
|
||||
});
|
||||
|
||||
return {
|
||||
profileCount: profiles,
|
||||
acceptedMembershipCount: acceptedMemberships,
|
||||
pendingMembershipCount: pendingMemberships,
|
||||
};
|
||||
}
|
||||
|
||||
// Test data creation helpers with unique identifiers
|
||||
function generateUniqueId() {
|
||||
return `${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
||||
}
|
||||
|
||||
async function createTestUser(data: { email: string; username: string; name?: string }): Promise<User> {
|
||||
const uniqueId = generateUniqueId();
|
||||
const uniqueEmail = data.email.includes("@")
|
||||
? data.email.replace("@", `-${uniqueId}@`)
|
||||
: `${data.email}-${uniqueId}@example.com`;
|
||||
const uniqueUsername = `${data.username}-${uniqueId}`;
|
||||
|
||||
// Pre-emptive cleanup to avoid conflicts
|
||||
try {
|
||||
await prisma.user.deleteMany({
|
||||
where: { OR: [{ email: uniqueEmail }, { username: uniqueUsername }] },
|
||||
});
|
||||
} catch {}
|
||||
|
||||
return await prisma.user.create({
|
||||
data: {
|
||||
email: uniqueEmail,
|
||||
username: uniqueUsername,
|
||||
name: data.name || uniqueUsername,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function createTestTeam(data: {
|
||||
name: string;
|
||||
slug: string;
|
||||
isOrganization?: boolean;
|
||||
parentId?: number;
|
||||
metadata?: any;
|
||||
organizationSettings?: {
|
||||
orgAutoAcceptEmail: string;
|
||||
isOrganizationVerified?: boolean;
|
||||
};
|
||||
}): Promise<Team> {
|
||||
const uniqueId = generateUniqueId();
|
||||
const uniqueSlug = `${data.slug}-${uniqueId}`;
|
||||
|
||||
// Pre-emptive cleanup to avoid conflicts
|
||||
try {
|
||||
await prisma.team.deleteMany({
|
||||
where: { slug: uniqueSlug },
|
||||
});
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
const team = await prisma.team.create({
|
||||
data: {
|
||||
name: `${data.name} ${uniqueId}`,
|
||||
slug: uniqueSlug,
|
||||
isOrganization: data.isOrganization || false,
|
||||
parentId: data.parentId,
|
||||
metadata: data.metadata,
|
||||
},
|
||||
});
|
||||
|
||||
// If this is an organization and organizationSettings are provided, create them
|
||||
if (data.isOrganization && data.organizationSettings) {
|
||||
await prisma.organizationSettings.create({
|
||||
data: {
|
||||
organizationId: team.id,
|
||||
orgAutoAcceptEmail: data.organizationSettings.orgAutoAcceptEmail,
|
||||
isOrganizationVerified: data.organizationSettings.isOrganizationVerified,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
// Mock translations fetch
|
||||
vi.mock("node-fetch", () => ({
|
||||
default: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
})
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock email sending
|
||||
vi.mock("@calcom/emails", () => ({
|
||||
sendTeamInviteEmail: vi.fn(() => Promise.resolve()),
|
||||
sendOrganizationAutoJoinEmail: vi.fn(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
// Mock for getTranslation
|
||||
vi.mock("@calcom/lib/server/i18n", () => ({
|
||||
getTranslation: vi.fn(() => Promise.resolve((key: string) => key)),
|
||||
}));
|
||||
|
||||
describe("inviteMember.handler Integration Tests", () => {
|
||||
// Track created test data for cleanup
|
||||
let testUsers: User[] = [];
|
||||
let testTeams: Team[] = [];
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset tracking arrays
|
||||
testUsers = [];
|
||||
testTeams = [];
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up test data in reverse dependency order
|
||||
const userIds = testUsers.map((u) => u.id);
|
||||
const teamIds = testTeams.map((t) => t.id);
|
||||
|
||||
if (userIds.length > 0 || teamIds.length > 0) {
|
||||
await prisma.$transaction([
|
||||
// Clean verification tokens for test users
|
||||
prisma.verificationToken.deleteMany({
|
||||
where: { identifier: { in: testUsers.map((u) => u.email) } },
|
||||
}),
|
||||
// Clean memberships
|
||||
prisma.membership.deleteMany({
|
||||
where: {
|
||||
OR: [{ userId: { in: userIds } }, { teamId: { in: teamIds } }],
|
||||
},
|
||||
}),
|
||||
// Clean profiles
|
||||
prisma.profile.deleteMany({
|
||||
where: {
|
||||
OR: [{ userId: { in: userIds } }, { organizationId: { in: teamIds } }],
|
||||
},
|
||||
}),
|
||||
// Clean teams
|
||||
prisma.team.deleteMany({
|
||||
where: { id: { in: teamIds } },
|
||||
}),
|
||||
// Clean users
|
||||
prisma.user.deleteMany({
|
||||
where: { id: { in: userIds } },
|
||||
}),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to track created entities
|
||||
function trackUser(user: User): User {
|
||||
testUsers.push(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
function trackTeam(team: Team): Team {
|
||||
testTeams.push(team);
|
||||
return team;
|
||||
}
|
||||
|
||||
// Helper to create a proper user context for inviteMemberHandler
|
||||
function createUserContext(user: User, organizationId: number | null = null): NonNullable<TrpcSessionUser> {
|
||||
const baseUser = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username || "",
|
||||
name: user.name,
|
||||
emailVerified: user.emailVerified,
|
||||
avatarUrl: user.avatarUrl,
|
||||
avatar: `https://example.com/avatar.png`,
|
||||
locale: user.locale || "en",
|
||||
timeZone: user.timeZone || "UTC",
|
||||
role: user.role,
|
||||
allowDynamicBooking: user.allowDynamicBooking || true,
|
||||
completedOnboarding: user.completedOnboarding,
|
||||
twoFactorEnabled: user.twoFactorEnabled,
|
||||
identityProvider: user.identityProvider,
|
||||
brandColor: user.brandColor,
|
||||
darkBrandColor: user.darkBrandColor,
|
||||
theme: user.theme,
|
||||
createdDate: user.createdDate,
|
||||
disableImpersonation: user.disableImpersonation,
|
||||
movedToProfileId: user.movedToProfileId,
|
||||
organizationId,
|
||||
organization: organizationId
|
||||
? {
|
||||
id: organizationId,
|
||||
isOrgAdmin: true,
|
||||
metadata: {},
|
||||
requestedSlug: null,
|
||||
}
|
||||
: {
|
||||
id: null,
|
||||
isOrgAdmin: false,
|
||||
metadata: {},
|
||||
requestedSlug: null,
|
||||
},
|
||||
defaultBookerLayouts: null,
|
||||
profile: organizationId
|
||||
? {
|
||||
id: user.id,
|
||||
upId: `${user.username}-${organizationId}`,
|
||||
username: user.username || "",
|
||||
userId: user.id,
|
||||
organizationId,
|
||||
organization: {
|
||||
id: organizationId,
|
||||
name: "Test Organization",
|
||||
slug: "test-org",
|
||||
isOrganization: true,
|
||||
metadata: {},
|
||||
},
|
||||
}
|
||||
: {
|
||||
id: user.id,
|
||||
upId: `${user.username}-default`,
|
||||
username: user.username || "",
|
||||
userId: user.id,
|
||||
organizationId: null,
|
||||
organization: null,
|
||||
},
|
||||
};
|
||||
|
||||
return baseUser as unknown as NonNullable<TrpcSessionUser>;
|
||||
}
|
||||
|
||||
describe("Organization Direct Invite Flow", () => {
|
||||
it("should not auto-accept user's membership that was unaccepted when migrating to org with non-matching autoAcceptEmailDomain", async () => {
|
||||
const organization = trackTeam(
|
||||
await createTestTeam({
|
||||
name: "Test Organization",
|
||||
slug: "test-org",
|
||||
isOrganization: true,
|
||||
metadata: {},
|
||||
organizationSettings: {
|
||||
orgAutoAcceptEmail: "company.com",
|
||||
isOrganizationVerified: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const team = trackTeam(
|
||||
await createTestTeam({
|
||||
name: "Test Team",
|
||||
slug: "test-team",
|
||||
isOrganization: false,
|
||||
parentId: organization.id,
|
||||
})
|
||||
);
|
||||
|
||||
const inviterUser = trackUser(
|
||||
await createTestUser({
|
||||
email: "inviter@company.com",
|
||||
username: "inviter",
|
||||
})
|
||||
);
|
||||
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
userId: inviterUser.id,
|
||||
teamId: organization.id,
|
||||
role: MembershipRole.OWNER,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Act: Invite user with non-matching domain
|
||||
const nonMatchingUser = trackUser(
|
||||
await createTestUser({
|
||||
email: "external@other.com",
|
||||
username: "external",
|
||||
})
|
||||
);
|
||||
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
userId: nonMatchingUser.id,
|
||||
teamId: team.id,
|
||||
role: MembershipRole.MEMBER,
|
||||
accepted: false,
|
||||
},
|
||||
});
|
||||
|
||||
await inviteMemberHandler({
|
||||
ctx: {
|
||||
user: createUserContext(inviterUser, organization.id),
|
||||
session: {} as any,
|
||||
} as any,
|
||||
input: {
|
||||
teamId: organization.id,
|
||||
usernameOrEmail: nonMatchingUser.email,
|
||||
role: MembershipRole.MEMBER,
|
||||
language: "en",
|
||||
creationSource: "WEBAPP" as const,
|
||||
},
|
||||
});
|
||||
|
||||
// Assert: No profile should be created
|
||||
const profile = await verifyProfileExists(nonMatchingUser.id, organization.id);
|
||||
expect(profile).toBeFalsy();
|
||||
|
||||
// Membership should not be auto-accepted
|
||||
const membership = await verifyMembershipExists(nonMatchingUser.id, organization.id);
|
||||
expect(membership).toBeTruthy();
|
||||
expect(membership?.accepted).toBe(false);
|
||||
|
||||
// Verify that the user is not a member of the organization
|
||||
const nonMatchingUserMembershipWithTeam = await verifyMembershipExists(nonMatchingUser.id, team.id);
|
||||
expect(nonMatchingUserMembershipWithTeam).toBeTruthy();
|
||||
expect(nonMatchingUserMembershipWithTeam?.accepted).toBe(false);
|
||||
});
|
||||
|
||||
it("should auto-accept user's membership that was unaccepted when migrating to org with matching autoAcceptEmailDomain", async () => {
|
||||
// Setup: User with unverified email who has an unaccepted team membership
|
||||
const unverifiedUserWithUnacceptedMembership = trackUser(
|
||||
await createTestUser({
|
||||
email: "john.doe@acme.com",
|
||||
username: "john.doe",
|
||||
})
|
||||
);
|
||||
|
||||
// Add unverified email status - will need to update in DB
|
||||
await prisma.user.update({
|
||||
where: { id: unverifiedUserWithUnacceptedMembership.id },
|
||||
data: { emailVerified: null },
|
||||
});
|
||||
|
||||
const organization = trackTeam(
|
||||
await createTestTeam({
|
||||
name: "Acme",
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
metadata: {},
|
||||
organizationSettings: {
|
||||
orgAutoAcceptEmail: "acme.com", // Matches user's email domain
|
||||
isOrganizationVerified: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Create a regular team first where user has unaccepted membership
|
||||
const regularTeam = trackTeam(
|
||||
await createTestTeam({
|
||||
name: "Regular Team",
|
||||
slug: "regular-team",
|
||||
isOrganization: false,
|
||||
parentId: organization.id,
|
||||
})
|
||||
);
|
||||
|
||||
// Add unaccepted team membership
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
teamId: regularTeam.id,
|
||||
userId: unverifiedUserWithUnacceptedMembership.id,
|
||||
accepted: false, // KEY: Unaccepted membership
|
||||
role: MembershipRole.MEMBER,
|
||||
},
|
||||
});
|
||||
|
||||
// Act: Simulate migration by inviting the user to the organization
|
||||
await inviteMembersWithNoInviterPermissionCheck({
|
||||
inviterName: null,
|
||||
teamId: organization.id,
|
||||
language: "en",
|
||||
creationSource: "WEBAPP" as const,
|
||||
orgSlug: organization.slug,
|
||||
invitations: [
|
||||
{
|
||||
usernameOrEmail: unverifiedUserWithUnacceptedMembership.email,
|
||||
role: MembershipRole.MEMBER,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const orgMembership = await verifyMembershipExists(
|
||||
unverifiedUserWithUnacceptedMembership.id,
|
||||
organization.id
|
||||
);
|
||||
const profile = await verifyProfileExists(unverifiedUserWithUnacceptedMembership.id, organization.id);
|
||||
|
||||
expect(profile?.userId).toBe(unverifiedUserWithUnacceptedMembership.id);
|
||||
expect(profile?.organizationId).toBe(organization.id);
|
||||
|
||||
// Verify original team membership is accepted now
|
||||
const originalMembership = await verifyMembershipExists(
|
||||
unverifiedUserWithUnacceptedMembership.id,
|
||||
regularTeam.id
|
||||
);
|
||||
expect(originalMembership?.accepted).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* This file is deprecated in favour of inviteMember.handler.integration-test.ts
|
||||
*
|
||||
* It mocks a lot of things that are untested and integration tests make more sense for this handler
|
||||
*/
|
||||
import { scenarios as checkRateLimitAndThrowErrorScenarios } from "../../../../../../../tests/libs/__mocks__/checkRateLimitAndThrowError";
|
||||
import { mock as getTranslationMock } from "../../../../../../../tests/libs/__mocks__/getTranslation";
|
||||
import {
|
||||
@@ -409,7 +414,7 @@ describe("inviteMemberHandler", () => {
|
||||
|
||||
// Call inviteMembersWithNoInviterPermissionCheck directly with isDirectUserAction=false
|
||||
const { inviteMembersWithNoInviterPermissionCheck } = await import("./inviteMember.handler");
|
||||
|
||||
|
||||
const result = await inviteMembersWithNoInviterPermissionCheck({
|
||||
inviterName: loggedInUser.name,
|
||||
teamId: team.id,
|
||||
@@ -487,7 +492,7 @@ describe("inviteMemberHandler", () => {
|
||||
|
||||
// Call inviteMembersWithNoInviterPermissionCheck directly with isDirectUserAction=false
|
||||
const { inviteMembersWithNoInviterPermissionCheck } = await import("./inviteMember.handler");
|
||||
|
||||
|
||||
const result = await inviteMembersWithNoInviterPermissionCheck({
|
||||
inviterName: "Test Inviter",
|
||||
teamId: team.id,
|
||||
@@ -517,6 +522,7 @@ describe("inviteMemberHandler", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("When rate limit exceeded, it should throw error", async () => {
|
||||
const userToBeInvited = buildExistingUser({
|
||||
id: 1,
|
||||
|
||||
@@ -39,6 +39,9 @@ const mockedReturnSuccessCheckPerms = {
|
||||
role: MembershipRole.ADMIN,
|
||||
userId: 1,
|
||||
teamId: 1,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
customRoleId: null,
|
||||
team: {
|
||||
id: 1,
|
||||
name: "Team A",
|
||||
@@ -95,6 +98,11 @@ const mockedRegularTeam: TeamWithParent = {
|
||||
smsLockState: "LOCKED",
|
||||
createdByOAuthClientId: null,
|
||||
smsLockReviewedByAdmin: false,
|
||||
hideTeamProfileLink: false,
|
||||
rrResetInterval: null,
|
||||
rrTimestampBasis: "CREATED_AT",
|
||||
bookingLimits: null,
|
||||
includeManagedEventsInLimits: false,
|
||||
};
|
||||
|
||||
const mockedSubTeam = {
|
||||
@@ -263,9 +271,13 @@ describe("Invite Member Utils", () => {
|
||||
adminGetsNoSlotsNotification: false,
|
||||
isAdminReviewed: false,
|
||||
isAdminAPIEnabled: false,
|
||||
allowSEOIndexing: false,
|
||||
orgProfileRedirectsToVerifiedDomain: false,
|
||||
disablePhoneOnlySMSNotifications: false,
|
||||
},
|
||||
slug: "abc",
|
||||
parent: null,
|
||||
isOrganization: true,
|
||||
};
|
||||
const result = getOrgState(true, { ...mockedRegularTeam, ...team });
|
||||
expect(result).toEqual({
|
||||
@@ -293,6 +305,9 @@ describe("Invite Member Utils", () => {
|
||||
adminGetsNoSlotsNotification: false,
|
||||
isAdminReviewed: false,
|
||||
isAdminAPIEnabled: false,
|
||||
allowSEOIndexing: false,
|
||||
orgProfileRedirectsToVerifiedDomain: false,
|
||||
disablePhoneOnlySMSNotifications: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -410,6 +425,7 @@ describe("Invite Member Utils", () => {
|
||||
...mockedRegularTeam,
|
||||
parentId: null,
|
||||
id: inviteeOrgId,
|
||||
isOrganization: true,
|
||||
};
|
||||
expect(canBeInvited(inviteeWithOrg, organization)).toBe(INVITE_STATUS.USER_ALREADY_INVITED_OR_MEMBER);
|
||||
});
|
||||
@@ -445,12 +461,33 @@ describe("Invite Member Utils", () => {
|
||||
const organization = {
|
||||
...mockedRegularTeam,
|
||||
id: organizationIdBeingInvitedTo,
|
||||
isOrganization: true,
|
||||
};
|
||||
expect(canBeInvited(inviteeWithOrg, organization)).toBe(
|
||||
INVITE_STATUS.USER_MEMBER_OF_OTHER_ORGANIZATION
|
||||
);
|
||||
});
|
||||
|
||||
it("should return CAN_BE_INVITED if the user being invited has a profile with the organization already", () => {
|
||||
const organizationId = 3;
|
||||
const inviteeWithOrg: UserWithMembership = {
|
||||
...invitee,
|
||||
profiles: [
|
||||
getSampleProfile({
|
||||
organizationId: organizationId,
|
||||
}),
|
||||
],
|
||||
teams: [],
|
||||
};
|
||||
|
||||
const organization = {
|
||||
...mockedRegularTeam,
|
||||
id: organizationId,
|
||||
isOrganization: true,
|
||||
};
|
||||
expect(canBeInvited(inviteeWithOrg, organization)).toBe(INVITE_STATUS.CAN_BE_INVITED);
|
||||
});
|
||||
|
||||
it("should return USER_MEMBER_OF_OTHER_ORGANIZATION if the invitee is being invited to a sub-team in an organization but he belongs to another organization", () => {
|
||||
const inviteeOrganizationId = 2;
|
||||
const subTeamOrganizationId = 3;
|
||||
|
||||
@@ -160,10 +160,27 @@ export function canBeInvited(invitee: UserWithMembership, team: TeamWithParent)
|
||||
return INVITE_STATUS.USER_PENDING_MEMBER_OF_THE_ORG;
|
||||
}
|
||||
|
||||
const hasDifferentOrganizationProfile = invitee.profiles.some((profile) => {
|
||||
const isRegularTeam = !team.isOrganization && !team.parentId;
|
||||
if (isRegularTeam) {
|
||||
// ⚠️ Inviting to a regular team but the user has a profile with some organization
|
||||
return true;
|
||||
}
|
||||
|
||||
const isOrganization = team.isOrganization && !team.parentId;
|
||||
if (isOrganization) {
|
||||
// ⚠️ User has profile with different organization than the organization being invited to
|
||||
return profile.organizationId !== team.id;
|
||||
}
|
||||
|
||||
// ⚠️ User having profile with an organization is invited to join a sub-team that is not part of the organization
|
||||
return profile.organizationId != team.parentId;
|
||||
});
|
||||
|
||||
if (
|
||||
!ENABLE_PROFILE_SWITCHER &&
|
||||
// Member of an organization is invited to join a team that is not a subteam of the organization
|
||||
invitee.profiles.find((profile) => profile.organizationId != team.parentId)
|
||||
// User having profile with an organization is invited to join a sub-team that is not part of the organization
|
||||
hasDifferentOrganizationProfile
|
||||
) {
|
||||
return INVITE_STATUS.USER_MEMBER_OF_OTHER_ORGANIZATION;
|
||||
}
|
||||
@@ -828,7 +845,7 @@ export async function handleExistingUsersInvites({
|
||||
if (parentOrganization) {
|
||||
const parsedOrg = getParsedTeam(parentOrganization);
|
||||
// Create profiles if needed
|
||||
await Promise.all([
|
||||
await Promise.all(
|
||||
autoJoinUsers
|
||||
.concat(regularUsers)
|
||||
.filter((u) => u.needToCreateProfile)
|
||||
@@ -841,8 +858,8 @@ export async function handleExistingUsersInvites({
|
||||
},
|
||||
organizationId: parsedOrg.id,
|
||||
})
|
||||
),
|
||||
]);
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const organization = team;
|
||||
@@ -877,6 +894,22 @@ export async function handleExistingUsersInvites({
|
||||
role: user.newRole,
|
||||
},
|
||||
});
|
||||
|
||||
// If auto-accepting into org, also accept any pending sub-team memberships
|
||||
if (shouldAutoAccept) {
|
||||
await prisma.membership.updateMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
accepted: false,
|
||||
team: {
|
||||
parentId: organization.id,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
return {
|
||||
...user,
|
||||
profile,
|
||||
|
||||
@@ -106,7 +106,7 @@ export const Select = <
|
||||
),
|
||||
indicatorsContainer: (state) =>
|
||||
cx(
|
||||
"flex items-center justify-center mt-0.5",
|
||||
"flex !items-start justify-center mt-0.5 h-full",
|
||||
state.selectProps.menuIsOpen
|
||||
? hasMultiLastIcons
|
||||
? "[&>*:last-child]:rotate-180 [&>*:last-child]:transition-transform [&>*:last-child]:w-4 [&>*:last-child]:h-4"
|
||||
|
||||
@@ -2500,7 +2500,7 @@ __metadata:
|
||||
"@axiomhq/winston": ^1.2.0
|
||||
"@calcom/platform-constants": "*"
|
||||
"@calcom/platform-enums": "*"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.300"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.306"
|
||||
"@calcom/platform-types": "*"
|
||||
"@calcom/platform-utils": "*"
|
||||
"@calcom/prisma": "*"
|
||||
@@ -2590,7 +2590,7 @@ __metadata:
|
||||
"@sentry/nextjs": ^9.15.0
|
||||
bcryptjs: ^2.4.3
|
||||
memory-cache: ^0.2.0
|
||||
next: ^15.3.0
|
||||
next: ^15.4.5
|
||||
next-api-middleware: ^1.0.1
|
||||
next-axiom: ^0.17.0
|
||||
next-swagger-doc: ^0.3.6
|
||||
@@ -3559,13 +3559,13 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.300":
|
||||
version: 0.0.300
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.300"
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.306":
|
||||
version: 0.0.306
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.306"
|
||||
dependencies:
|
||||
"@calcom/features": "*"
|
||||
"@calcom/lib": "*"
|
||||
checksum: bff9f08a0586169f2d5a85167f0a0e0e57ca93b637ca858a72b5d3d943ceef37d652efa3d2e6f7561548e4dfe3aabb5c0c6e0a8d38f68d3d21d5e6993e56ad7d
|
||||
checksum: 602b8b446508429096b8e03e9edbe1ff744cf7c7af3fbe8a683aaaf0d3c96d1cd30ff356f72682449ca072beadaae95fdaa5c44b26e30cfce3bb838f519216f9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4115,7 +4115,7 @@ __metadata:
|
||||
mime-types: ^2.1.35
|
||||
module-alias: ^2.2.2
|
||||
msw: ^0.42.3
|
||||
next: ^15.3.0
|
||||
next: ^15.4.5
|
||||
next-auth: ^4.22.1
|
||||
next-axiom: ^0.17.0
|
||||
next-collect: ^0.2.1
|
||||
@@ -4711,12 +4711,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/runtime@npm:^1.4.0":
|
||||
version: 1.4.3
|
||||
resolution: "@emnapi/runtime@npm:1.4.3"
|
||||
"@emnapi/runtime@npm:^1.4.4":
|
||||
version: 1.4.5
|
||||
resolution: "@emnapi/runtime@npm:1.4.5"
|
||||
dependencies:
|
||||
tslib: ^2.4.0
|
||||
checksum: ff2074809638ed878e476ece370c6eae7e6257bf029a581bb7a290488d8f2a08c420a65988c7f03bfc6bb689218f0cd995d2f935bd182150b357fc2341142f4f
|
||||
checksum: 99ab25d55cf1ceeec12f83b60f48e744f8e1dfc8d52a2ed81b3b09bf15182e61ef55f25b69d51ec83044861bddaa4404e7c3285bf71dd518a7980867e41c2a10
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -6724,11 +6724,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-darwin-arm64@npm:0.34.1":
|
||||
version: 0.34.1
|
||||
resolution: "@img/sharp-darwin-arm64@npm:0.34.1"
|
||||
"@img/sharp-darwin-arm64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-darwin-arm64@npm:0.34.3"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-darwin-arm64": 1.1.0
|
||||
"@img/sharp-libvips-darwin-arm64": 1.2.0
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-darwin-arm64":
|
||||
optional: true
|
||||
@@ -6748,11 +6748,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-darwin-x64@npm:0.34.1":
|
||||
version: 0.34.1
|
||||
resolution: "@img/sharp-darwin-x64@npm:0.34.1"
|
||||
"@img/sharp-darwin-x64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-darwin-x64@npm:0.34.3"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-darwin-x64": 1.1.0
|
||||
"@img/sharp-libvips-darwin-x64": 1.2.0
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-darwin-x64":
|
||||
optional: true
|
||||
@@ -6767,9 +6767,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64@npm:1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@img/sharp-libvips-darwin-arm64@npm:1.1.0"
|
||||
"@img/sharp-libvips-darwin-arm64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-darwin-arm64@npm:1.2.0"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -6781,9 +6781,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-darwin-x64@npm:1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@img/sharp-libvips-darwin-x64@npm:1.1.0"
|
||||
"@img/sharp-libvips-darwin-x64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-darwin-x64@npm:1.2.0"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -6795,9 +6795,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-arm64@npm:1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@img/sharp-libvips-linux-arm64@npm:1.1.0"
|
||||
"@img/sharp-libvips-linux-arm64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linux-arm64@npm:1.2.0"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -6809,16 +6809,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-arm@npm:1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@img/sharp-libvips-linux-arm@npm:1.1.0"
|
||||
"@img/sharp-libvips-linux-arm@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linux-arm@npm:1.2.0"
|
||||
conditions: os=linux & cpu=arm & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64@npm:1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@img/sharp-libvips-linux-ppc64@npm:1.1.0"
|
||||
"@img/sharp-libvips-linux-ppc64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linux-ppc64@npm:1.2.0"
|
||||
conditions: os=linux & cpu=ppc64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -6830,9 +6830,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-s390x@npm:1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@img/sharp-libvips-linux-s390x@npm:1.1.0"
|
||||
"@img/sharp-libvips-linux-s390x@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linux-s390x@npm:1.2.0"
|
||||
conditions: os=linux & cpu=s390x & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -6844,9 +6844,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-x64@npm:1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@img/sharp-libvips-linux-x64@npm:1.1.0"
|
||||
"@img/sharp-libvips-linux-x64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linux-x64@npm:1.2.0"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -6858,9 +6858,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64@npm:1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.1.0"
|
||||
"@img/sharp-libvips-linuxmusl-arm64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.2.0"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -6872,9 +6872,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64@npm:1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.1.0"
|
||||
"@img/sharp-libvips-linuxmusl-x64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.2.0"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -6891,11 +6891,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-arm64@npm:0.34.1":
|
||||
version: 0.34.1
|
||||
resolution: "@img/sharp-linux-arm64@npm:0.34.1"
|
||||
"@img/sharp-linux-arm64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linux-arm64@npm:0.34.3"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-arm64": 1.1.0
|
||||
"@img/sharp-libvips-linux-arm64": 1.2.0
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-arm64":
|
||||
optional: true
|
||||
@@ -6915,11 +6915,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-arm@npm:0.34.1":
|
||||
version: 0.34.1
|
||||
resolution: "@img/sharp-linux-arm@npm:0.34.1"
|
||||
"@img/sharp-linux-arm@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linux-arm@npm:0.34.3"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-arm": 1.1.0
|
||||
"@img/sharp-libvips-linux-arm": 1.2.0
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-arm":
|
||||
optional: true
|
||||
@@ -6927,6 +6927,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-ppc64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linux-ppc64@npm:0.34.3"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-ppc64": 1.2.0
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-ppc64":
|
||||
optional: true
|
||||
conditions: os=linux & cpu=ppc64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-s390x@npm:0.33.5":
|
||||
version: 0.33.5
|
||||
resolution: "@img/sharp-linux-s390x@npm:0.33.5"
|
||||
@@ -6939,11 +6951,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-s390x@npm:0.34.1":
|
||||
version: 0.34.1
|
||||
resolution: "@img/sharp-linux-s390x@npm:0.34.1"
|
||||
"@img/sharp-linux-s390x@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linux-s390x@npm:0.34.3"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-s390x": 1.1.0
|
||||
"@img/sharp-libvips-linux-s390x": 1.2.0
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-s390x":
|
||||
optional: true
|
||||
@@ -6963,11 +6975,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-x64@npm:0.34.1":
|
||||
version: 0.34.1
|
||||
resolution: "@img/sharp-linux-x64@npm:0.34.1"
|
||||
"@img/sharp-linux-x64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linux-x64@npm:0.34.3"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-x64": 1.1.0
|
||||
"@img/sharp-libvips-linux-x64": 1.2.0
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-x64":
|
||||
optional: true
|
||||
@@ -6987,11 +6999,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linuxmusl-arm64@npm:0.34.1":
|
||||
version: 0.34.1
|
||||
resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.1"
|
||||
"@img/sharp-linuxmusl-arm64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.3"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linuxmusl-arm64": 1.1.0
|
||||
"@img/sharp-libvips-linuxmusl-arm64": 1.2.0
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linuxmusl-arm64":
|
||||
optional: true
|
||||
@@ -7011,11 +7023,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linuxmusl-x64@npm:0.34.1":
|
||||
version: 0.34.1
|
||||
resolution: "@img/sharp-linuxmusl-x64@npm:0.34.1"
|
||||
"@img/sharp-linuxmusl-x64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linuxmusl-x64@npm:0.34.3"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linuxmusl-x64": 1.1.0
|
||||
"@img/sharp-libvips-linuxmusl-x64": 1.2.0
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linuxmusl-x64":
|
||||
optional: true
|
||||
@@ -7032,15 +7044,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-wasm32@npm:0.34.1":
|
||||
version: 0.34.1
|
||||
resolution: "@img/sharp-wasm32@npm:0.34.1"
|
||||
"@img/sharp-wasm32@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-wasm32@npm:0.34.3"
|
||||
dependencies:
|
||||
"@emnapi/runtime": ^1.4.0
|
||||
"@emnapi/runtime": ^1.4.4
|
||||
conditions: cpu=wasm32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-win32-arm64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-win32-arm64@npm:0.34.3"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-win32-ia32@npm:0.33.5":
|
||||
version: 0.33.5
|
||||
resolution: "@img/sharp-win32-ia32@npm:0.33.5"
|
||||
@@ -7048,9 +7067,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-win32-ia32@npm:0.34.1":
|
||||
version: 0.34.1
|
||||
resolution: "@img/sharp-win32-ia32@npm:0.34.1"
|
||||
"@img/sharp-win32-ia32@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-win32-ia32@npm:0.34.3"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -7062,9 +7081,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-win32-x64@npm:0.34.1":
|
||||
version: 0.34.1
|
||||
resolution: "@img/sharp-win32-x64@npm:0.34.1"
|
||||
"@img/sharp-win32-x64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-win32-x64@npm:0.34.3"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -8940,10 +8959,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/env@npm:15.3.3":
|
||||
version: 15.3.3
|
||||
resolution: "@next/env@npm:15.3.3"
|
||||
checksum: 52513b04ff1951ad094b37a89dcd37e9275c2d6c635dbe7fbdc44bd39b5511eb2c7c537a80ebbf92d197e5f1293ff16c175cd5dccc9c54bbfaac7271c1fe1bb9
|
||||
"@next/env@npm:15.4.6":
|
||||
version: 15.4.6
|
||||
resolution: "@next/env@npm:15.4.6"
|
||||
checksum: bf1ea15f2a86c6bc968d92d4fb778d106e6e94c31893f51699f0fc51fd17416ae9e4086176164e82430c84597d509b77c2c5738e8cd98bc7f73ebc6d83b2926f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8995,9 +9014,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-darwin-arm64@npm:15.3.3":
|
||||
version: 15.3.3
|
||||
resolution: "@next/swc-darwin-arm64@npm:15.3.3"
|
||||
"@next/swc-darwin-arm64@npm:15.4.6":
|
||||
version: 15.4.6
|
||||
resolution: "@next/swc-darwin-arm64@npm:15.4.6"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -9023,9 +9042,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-darwin-x64@npm:15.3.3":
|
||||
version: 15.3.3
|
||||
resolution: "@next/swc-darwin-x64@npm:15.3.3"
|
||||
"@next/swc-darwin-x64@npm:15.4.6":
|
||||
version: 15.4.6
|
||||
resolution: "@next/swc-darwin-x64@npm:15.4.6"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -9051,9 +9070,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-linux-arm64-gnu@npm:15.3.3":
|
||||
version: 15.3.3
|
||||
resolution: "@next/swc-linux-arm64-gnu@npm:15.3.3"
|
||||
"@next/swc-linux-arm64-gnu@npm:15.4.6":
|
||||
version: 15.4.6
|
||||
resolution: "@next/swc-linux-arm64-gnu@npm:15.4.6"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -9079,9 +9098,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-linux-arm64-musl@npm:15.3.3":
|
||||
version: 15.3.3
|
||||
resolution: "@next/swc-linux-arm64-musl@npm:15.3.3"
|
||||
"@next/swc-linux-arm64-musl@npm:15.4.6":
|
||||
version: 15.4.6
|
||||
resolution: "@next/swc-linux-arm64-musl@npm:15.4.6"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -9107,9 +9126,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-linux-x64-gnu@npm:15.3.3":
|
||||
version: 15.3.3
|
||||
resolution: "@next/swc-linux-x64-gnu@npm:15.3.3"
|
||||
"@next/swc-linux-x64-gnu@npm:15.4.6":
|
||||
version: 15.4.6
|
||||
resolution: "@next/swc-linux-x64-gnu@npm:15.4.6"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -9135,9 +9154,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-linux-x64-musl@npm:15.3.3":
|
||||
version: 15.3.3
|
||||
resolution: "@next/swc-linux-x64-musl@npm:15.3.3"
|
||||
"@next/swc-linux-x64-musl@npm:15.4.6":
|
||||
version: 15.4.6
|
||||
resolution: "@next/swc-linux-x64-musl@npm:15.4.6"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -9163,9 +9182,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-win32-arm64-msvc@npm:15.3.3":
|
||||
version: 15.3.3
|
||||
resolution: "@next/swc-win32-arm64-msvc@npm:15.3.3"
|
||||
"@next/swc-win32-arm64-msvc@npm:15.4.6":
|
||||
version: 15.4.6
|
||||
resolution: "@next/swc-win32-arm64-msvc@npm:15.4.6"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -9205,9 +9224,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-win32-x64-msvc@npm:15.3.3":
|
||||
version: 15.3.3
|
||||
resolution: "@next/swc-win32-x64-msvc@npm:15.3.3"
|
||||
"@next/swc-win32-x64-msvc@npm:15.4.6":
|
||||
version: 15.4.6
|
||||
resolution: "@next/swc-win32-x64-msvc@npm:15.4.6"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -24262,6 +24281,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-libc@npm:^2.0.4":
|
||||
version: 2.0.4
|
||||
resolution: "detect-libc@npm:2.0.4"
|
||||
checksum: 3d186b7d4e16965e10e21db596c78a4e131f9eee69c0081d13b85e6a61d7448d3ba23fe7997648022bdfa3b0eb4cc3c289a44c8188df949445a20852689abef6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-newline@npm:^3.0.0":
|
||||
version: 3.1.0
|
||||
resolution: "detect-newline@npm:3.1.0"
|
||||
@@ -35825,29 +35851,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"next@npm:^15.3.0":
|
||||
version: 15.3.3
|
||||
resolution: "next@npm:15.3.3"
|
||||
"next@npm:^15.4.5":
|
||||
version: 15.4.6
|
||||
resolution: "next@npm:15.4.6"
|
||||
dependencies:
|
||||
"@next/env": 15.3.3
|
||||
"@next/swc-darwin-arm64": 15.3.3
|
||||
"@next/swc-darwin-x64": 15.3.3
|
||||
"@next/swc-linux-arm64-gnu": 15.3.3
|
||||
"@next/swc-linux-arm64-musl": 15.3.3
|
||||
"@next/swc-linux-x64-gnu": 15.3.3
|
||||
"@next/swc-linux-x64-musl": 15.3.3
|
||||
"@next/swc-win32-arm64-msvc": 15.3.3
|
||||
"@next/swc-win32-x64-msvc": 15.3.3
|
||||
"@swc/counter": 0.1.3
|
||||
"@next/env": 15.4.6
|
||||
"@next/swc-darwin-arm64": 15.4.6
|
||||
"@next/swc-darwin-x64": 15.4.6
|
||||
"@next/swc-linux-arm64-gnu": 15.4.6
|
||||
"@next/swc-linux-arm64-musl": 15.4.6
|
||||
"@next/swc-linux-x64-gnu": 15.4.6
|
||||
"@next/swc-linux-x64-musl": 15.4.6
|
||||
"@next/swc-win32-arm64-msvc": 15.4.6
|
||||
"@next/swc-win32-x64-msvc": 15.4.6
|
||||
"@swc/helpers": 0.5.15
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: ^1.0.30001579
|
||||
postcss: 8.4.31
|
||||
sharp: ^0.34.1
|
||||
sharp: ^0.34.3
|
||||
styled-jsx: 5.1.6
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ^1.1.0
|
||||
"@playwright/test": ^1.41.2
|
||||
"@playwright/test": ^1.51.1
|
||||
babel-plugin-react-compiler: "*"
|
||||
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
@@ -35882,7 +35906,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
next: dist/bin/next
|
||||
checksum: 0e071a67e50b2770d27268b19dbe5a510297cf31fe3035ae169caa7e93f97bcf58c3f533e22fe300c8ad92b87e4f7d807619f7aa3b97a290de7c2b82b1451c4c
|
||||
checksum: 996e28076e30ae1e077219e46d2555f73391ca8132016c83b6096283f638bd6c7be58e2f320d0ef45ac0c18067138d603f900df50517d20ca804f102d2150d8a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -41705,7 +41729,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.1":
|
||||
"semver@npm:^7.6.0, semver@npm:^7.6.3":
|
||||
version: 7.7.1
|
||||
resolution: "semver@npm:7.7.1"
|
||||
bin:
|
||||
@@ -41714,6 +41738,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver@npm:^7.7.2":
|
||||
version: 7.7.2
|
||||
resolution: "semver@npm:7.7.2"
|
||||
bin:
|
||||
semver: bin/semver.js
|
||||
checksum: dd94ba8f1cbc903d8eeb4dd8bf19f46b3deb14262b6717d0de3c804b594058ae785ef2e4b46c5c3b58733c99c83339068203002f9e37cfe44f7e2cc5e3d2f621
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver@npm:~2.3.1":
|
||||
version: 2.3.2
|
||||
resolution: "semver@npm:2.3.2"
|
||||
@@ -41989,33 +42022,35 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sharp@npm:^0.34.1":
|
||||
version: 0.34.1
|
||||
resolution: "sharp@npm:0.34.1"
|
||||
"sharp@npm:^0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "sharp@npm:0.34.3"
|
||||
dependencies:
|
||||
"@img/sharp-darwin-arm64": 0.34.1
|
||||
"@img/sharp-darwin-x64": 0.34.1
|
||||
"@img/sharp-libvips-darwin-arm64": 1.1.0
|
||||
"@img/sharp-libvips-darwin-x64": 1.1.0
|
||||
"@img/sharp-libvips-linux-arm": 1.1.0
|
||||
"@img/sharp-libvips-linux-arm64": 1.1.0
|
||||
"@img/sharp-libvips-linux-ppc64": 1.1.0
|
||||
"@img/sharp-libvips-linux-s390x": 1.1.0
|
||||
"@img/sharp-libvips-linux-x64": 1.1.0
|
||||
"@img/sharp-libvips-linuxmusl-arm64": 1.1.0
|
||||
"@img/sharp-libvips-linuxmusl-x64": 1.1.0
|
||||
"@img/sharp-linux-arm": 0.34.1
|
||||
"@img/sharp-linux-arm64": 0.34.1
|
||||
"@img/sharp-linux-s390x": 0.34.1
|
||||
"@img/sharp-linux-x64": 0.34.1
|
||||
"@img/sharp-linuxmusl-arm64": 0.34.1
|
||||
"@img/sharp-linuxmusl-x64": 0.34.1
|
||||
"@img/sharp-wasm32": 0.34.1
|
||||
"@img/sharp-win32-ia32": 0.34.1
|
||||
"@img/sharp-win32-x64": 0.34.1
|
||||
"@img/sharp-darwin-arm64": 0.34.3
|
||||
"@img/sharp-darwin-x64": 0.34.3
|
||||
"@img/sharp-libvips-darwin-arm64": 1.2.0
|
||||
"@img/sharp-libvips-darwin-x64": 1.2.0
|
||||
"@img/sharp-libvips-linux-arm": 1.2.0
|
||||
"@img/sharp-libvips-linux-arm64": 1.2.0
|
||||
"@img/sharp-libvips-linux-ppc64": 1.2.0
|
||||
"@img/sharp-libvips-linux-s390x": 1.2.0
|
||||
"@img/sharp-libvips-linux-x64": 1.2.0
|
||||
"@img/sharp-libvips-linuxmusl-arm64": 1.2.0
|
||||
"@img/sharp-libvips-linuxmusl-x64": 1.2.0
|
||||
"@img/sharp-linux-arm": 0.34.3
|
||||
"@img/sharp-linux-arm64": 0.34.3
|
||||
"@img/sharp-linux-ppc64": 0.34.3
|
||||
"@img/sharp-linux-s390x": 0.34.3
|
||||
"@img/sharp-linux-x64": 0.34.3
|
||||
"@img/sharp-linuxmusl-arm64": 0.34.3
|
||||
"@img/sharp-linuxmusl-x64": 0.34.3
|
||||
"@img/sharp-wasm32": 0.34.3
|
||||
"@img/sharp-win32-arm64": 0.34.3
|
||||
"@img/sharp-win32-ia32": 0.34.3
|
||||
"@img/sharp-win32-x64": 0.34.3
|
||||
color: ^4.2.3
|
||||
detect-libc: ^2.0.3
|
||||
semver: ^7.7.1
|
||||
detect-libc: ^2.0.4
|
||||
semver: ^7.7.2
|
||||
dependenciesMeta:
|
||||
"@img/sharp-darwin-arm64":
|
||||
optional: true
|
||||
@@ -42043,6 +42078,8 @@ __metadata:
|
||||
optional: true
|
||||
"@img/sharp-linux-arm64":
|
||||
optional: true
|
||||
"@img/sharp-linux-ppc64":
|
||||
optional: true
|
||||
"@img/sharp-linux-s390x":
|
||||
optional: true
|
||||
"@img/sharp-linux-x64":
|
||||
@@ -42053,11 +42090,13 @@ __metadata:
|
||||
optional: true
|
||||
"@img/sharp-wasm32":
|
||||
optional: true
|
||||
"@img/sharp-win32-arm64":
|
||||
optional: true
|
||||
"@img/sharp-win32-ia32":
|
||||
optional: true
|
||||
"@img/sharp-win32-x64":
|
||||
optional: true
|
||||
checksum: ce2798a2d1a4e4fb9bae5642177e4681050897de8c2d6e1783c8c15d8a40cc0e22bf4ce91d1bf355afd133d16d7032fcfdfa2ed3305eab34b7f8de8f61175519
|
||||
checksum: f1d70751ab15080957f1a5db5583d00b002e644878e3e9a6d00bdbb31255f6e9df218ef82d96d3588b83023681f35bcf03e265a44e214b11306ef3fb439ac04d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user