Compare commits

..
Author SHA1 Message Date
github-actions[bot] e3fb7340aa chore: version packages (#23090)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-14 16:28:31 +02:00
Benny Joo e4ef2832cd chore: upgrade next.js to 15.4.5 (#23079)
* upgrade

* update yarn lock

* fix type check
2025-08-14 10:36:20 -03:00
Benny Joo 881602cf1c refactor: Get rid of usages of Next.js private interfaces in our codebase (#23080)
* chore: do not use nextjs private interfaces

* fix type check

* fix

* fix

* refactor
2025-08-14 09:51:49 -03:00
4c01f171ae fix: multiple widgets for Booker atom (#22925)
* fix manage test error

* fix manage test error

* update useBookerContext

* wrap store provider around booker

* replace useStore with useBookerStoreContext

* replace useBookerStore with useBookerStoreContext

* add initializer function for booker store provider

* update props

* fixup: pass more props

* export StoreInitializeType type

* replace useBookerStore with useBookerStoreContext

* fixup: dont wrap BookerStoreProvider around booker directly

* wrap BookerStoreProvider around booker web wrapper and fix local storage import

* fix: wrap test components with BookerStoreProvider to fix failing unit tests

- Create reusable test utility in test-utils.tsx with comprehensive mock store
- Update Booker.test.tsx to use context-based testing approach
- Fix DatePicker tests in both bookings and calendars packages
- Simulate auto-advance behavior for month navigation tests
- All 14 previously failing tests now pass

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* add changesets

* fix: complete migration from useBookerStore to useBookerStoreContext

- Updated all remaining files to use useBookerStoreContext instead of useBookerStore
- Fixed BookingFields.test.tsx by wrapping test component with BookerStoreProvider
- Ensures all Booker components work within the new context-based store pattern
- Resolves failing unit tests in CI by completing the store migration

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* fix: add missing useBookerStoreContext import in BookerWebWrapper.tsx

- Fixes TypeScript errors in CI where useBookerStoreContext was used but not imported
- Completes the migration from useBookerStore to useBookerStoreContext pattern
- All tests pass locally after this fix

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* Revert "fix: add missing useBookerStoreContext import in BookerWebWrapper.tsx"

This reverts commit 12947ca63f46c42e5aee74c78176ef230e2bf2cf.

* Revert "fix: complete migration from useBookerStore to useBookerStoreContext"

This reverts commit 87d837f2d30e4f9259d2be786049d21856f4eef0.

* fixup: useBookerStoreContext and useInitializeBookerStoreContext for booker web wrapper

* fixup: failing E2E tests

* fixup: email embed breaking

* fixup: troubleshooter crashing

* replace useBookerstore with useBookerStoreContext

---------

Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-08-14 17:57:59 +05:30
Anik Dhabal Babu d9817fcb07 fix: prevent cross-team app visibility in organizations (#23085) 2025-08-14 11:50:30 +00:00
Anik Dhabal Babu a784af3e47 fix: Add org-level payment credential fallback for no-show fee charging (#23071)
* fix: feat: Add org-level payment credential fallback for team bookings

* use repository pattern
2025-08-14 11:30:38 +00:00
Syed Ali Shahbaz c268925330 feat: org/team event type private links endpoints (#23048)
* init

* added test

* remove unnecessary comments

* remove unnecessary userId
2025-08-14 10:21:39 +00:00
Lauris Skraucis 378e8e0330 fix: managed organization bookings (#23078)
* fix: private link update input

* fix: scope admin memberships to org and org teams

* chore: update platform libraries
2025-08-14 09:30:37 +00:00
58 changed files with 3101 additions and 986 deletions
+1 -1
View File
@@ -33,7 +33,7 @@
"@sentry/nextjs": "^9.15.0", "@sentry/nextjs": "^9.15.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"next": "^15.3.0", "next": "^15.4.5",
"next-api-middleware": "^1.0.1", "next-api-middleware": "^1.0.1",
"next-axiom": "^0.17.0", "next-axiom": "^0.17.0",
"next-swagger-doc": "^0.3.6", "next-swagger-doc": "^0.3.6",
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0", "@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*", "@calcom/platform-constants": "*",
"@calcom/platform-enums": "*", "@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.300", "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.306",
"@calcom/platform-types": "*", "@calcom/platform-types": "*",
"@calcom/platform-utils": "*", "@calcom/platform-utils": "*",
"@calcom/prisma": "*", "@calcom/prisma": "*",
@@ -4,37 +4,21 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard"; import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
import { import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
Body, import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
UseGuards,
} from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags, OmitType } from "@nestjs/swagger";
import { import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
EVENT_TYPE_READ,
EVENT_TYPE_WRITE,
SUCCESS_STATUS,
} from "@calcom/platform-constants";
import { import {
CreatePrivateLinkInput, CreatePrivateLinkInput,
CreatePrivateLinkOutput, CreatePrivateLinkOutput,
DeletePrivateLinkOutput, DeletePrivateLinkOutput,
GetPrivateLinksOutput, GetPrivateLinksOutput,
UpdatePrivateLinkInput,
UpdatePrivateLinkOutput, UpdatePrivateLinkOutput,
UpdatePrivateLinkBody,
} from "@calcom/platform-types"; } from "@calcom/platform-types";
import { PrivateLinksService } from "../services/private-links.service"; import { PrivateLinksService } from "../services/private-links.service";
class UpdatePrivateLinkBody extends OmitType(UpdatePrivateLinkInput, ["linkId"] as const) {}
@Controller({ @Controller({
path: "/v2/event-types/:eventTypeId/private-links", path: "/v2/event-types/:eventTypeId/private-links",
}) })
@@ -85,11 +69,10 @@ export class EventTypesPrivateLinksController {
async updatePrivateLink( async updatePrivateLink(
@Param("eventTypeId", ParseIntPipe) eventTypeId: number, @Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@Param("linkId") linkId: string, @Param("linkId") linkId: string,
@Body() body: UpdatePrivateLinkBody, @Body() body: UpdatePrivateLinkBody
@GetUser("id") userId: number
): Promise<UpdatePrivateLinkOutput> { ): Promise<UpdatePrivateLinkOutput> {
const updateInput = { ...body, linkId }; const updateInput = { ...body, linkId };
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, userId, updateInput); const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, updateInput);
return { return {
status: SUCCESS_STATUS, status: SUCCESS_STATUS,
@@ -104,10 +87,9 @@ export class EventTypesPrivateLinksController {
@ApiOperation({ summary: "Delete a private link for an event type" }) @ApiOperation({ summary: "Delete a private link for an event type" })
async deletePrivateLink( async deletePrivateLink(
@Param("eventTypeId", ParseIntPipe) eventTypeId: number, @Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@Param("linkId") linkId: string, @Param("linkId") linkId: string
@GetUser("id") userId: number
): Promise<DeletePrivateLinkOutput> { ): Promise<DeletePrivateLinkOutput> {
await this.privateLinksService.deletePrivateLink(eventTypeId, userId, linkId); await this.privateLinksService.deletePrivateLink(eventTypeId, linkId);
return { return {
status: SUCCESS_STATUS, status: SUCCESS_STATUS,
@@ -118,5 +100,3 @@ export class EventTypesPrivateLinksController {
}; };
} }
} }
@@ -0,0 +1,121 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_API_KEY_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import {
CreatePrivateLinkInput,
CreatePrivateLinkOutput,
DeletePrivateLinkOutput,
GetPrivateLinksOutput,
UpdatePrivateLinkInput,
UpdatePrivateLinkOutput,
} from "@calcom/platform-types";
import { PrivateLinksService } from "../services/private-links.service";
@Controller({
path: "/v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links",
version: API_VERSIONS_VALUES,
})
@DocsTags("Orgs / Teams / Event Types / Private Links")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsEventTypesPrivateLinksController {
constructor(
private readonly privateLinksService: PrivateLinksService,
private readonly teamsEventTypesService: TeamsEventTypesService
) {}
@Post("/")
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@ApiOperation({ summary: "Create a private link for a team event type" })
async createPrivateLink(
@Param("teamId", ParseIntPipe) teamId: number,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@Body() body: CreatePrivateLinkInput
): Promise<CreatePrivateLinkOutput> {
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
// Use teamId as the seed for link generation in org/team context
const privateLink = await this.privateLinksService.createPrivateLink(eventTypeId, teamId, body);
return {
status: SUCCESS_STATUS,
data: privateLink,
};
}
@Get("/")
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@ApiOperation({ summary: "Get all private links for a team event type" })
async getPrivateLinks(
@Param("teamId", ParseIntPipe) teamId: number,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
): Promise<GetPrivateLinksOutput> {
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
const privateLinks = await this.privateLinksService.getPrivateLinks(eventTypeId);
return {
status: SUCCESS_STATUS,
data: privateLinks,
};
}
@Patch("/:linkId")
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@ApiOperation({ summary: "Update a private link for a team event type" })
async updatePrivateLink(
@Param("teamId", ParseIntPipe) teamId: number,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@Param("linkId") linkId: string,
@Body() body: Omit<UpdatePrivateLinkInput, "linkId">
): Promise<UpdatePrivateLinkOutput> {
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
const updateInput: UpdatePrivateLinkInput = { ...body, linkId };
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, updateInput);
return {
status: SUCCESS_STATUS,
data: privateLink,
};
}
@Delete("/:linkId")
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@ApiOperation({ summary: "Delete a private link for a team event type" })
async deletePrivateLink(
@Param("teamId", ParseIntPipe) teamId: number,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@Param("linkId") linkId: string
): Promise<DeletePrivateLinkOutput> {
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
await this.privateLinksService.deletePrivateLink(eventTypeId, linkId);
return {
status: SUCCESS_STATUS,
data: {
linkId,
message: "Private link deleted successfully",
},
};
}
}
@@ -12,7 +12,7 @@ import { PrivateLinksOutputService } from "./services/private-links-output.servi
import { PrivateLinksService } from "./services/private-links.service"; import { PrivateLinksService } from "./services/private-links.service";
@Module({ @Module({
imports: [TokensModule, OAuthClientModule, PrismaModule, EventTypesModule_2024_06_14], imports: [TokensModule, PrismaModule, EventTypesModule_2024_06_14],
controllers: [EventTypesPrivateLinksController], controllers: [EventTypesPrivateLinksController],
providers: [ providers: [
PrivateLinksService, PrivateLinksService,
@@ -21,5 +21,6 @@ import { PrivateLinksService } from "./services/private-links.service";
PrivateLinksRepository, PrivateLinksRepository,
EventTypeOwnershipGuard, EventTypeOwnershipGuard,
], ],
exports: [PrivateLinksService],
}) })
export class EventTypesPrivateLinksModule {} export class EventTypesPrivateLinksModule {}
@@ -68,11 +68,7 @@ export class PrivateLinksService {
} }
} }
async updatePrivateLink( async updatePrivateLink(eventTypeId: number, input: UpdatePrivateLinkInput): Promise<PrivateLinkOutput> {
eventTypeId: number,
userId: number,
input: UpdatePrivateLinkInput
): Promise<PrivateLinkOutput> {
try { try {
const transformedInput = this.inputService.transformUpdateInput(input); const transformedInput = this.inputService.transformUpdateInput(input);
const updatedResult = await this.repo.update(eventTypeId, { const updatedResult = await this.repo.update(eventTypeId, {
@@ -106,7 +102,7 @@ export class PrivateLinksService {
} }
} }
async deletePrivateLink(eventTypeId: number, userId: number, linkId: string): Promise<void> { async deletePrivateLink(eventTypeId: number, linkId: string): Promise<void> {
try { try {
const { count } = await this.repo.delete(eventTypeId, linkId); const { count } = await this.repo.delete(eventTypeId, linkId);
if (count === 0) { if (count === 0) {
@@ -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();
});
});
});
@@ -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 { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module"; import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller"; import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller";
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository"; import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer"; import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service"; import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service";
@@ -91,6 +93,7 @@ import { Module } from "@nestjs/common";
OrganizationsStripeModule, OrganizationsStripeModule,
OrganizationsTeamsRoutingFormsModule, OrganizationsTeamsRoutingFormsModule,
OrganizationsConferencingModule, OrganizationsConferencingModule,
EventTypesPrivateLinksModule,
], ],
providers: [ providers: [
OrganizationsRepository, OrganizationsRepository,
@@ -177,6 +180,7 @@ import { Module } from "@nestjs/common";
OrganizationsTeamsSchedulesController, OrganizationsTeamsSchedulesController,
OrganizationsUsersOOOController, OrganizationsUsersOOOController,
OrganizationTeamWorkflowsController, OrganizationTeamWorkflowsController,
OrganizationsEventTypesPrivateLinksController,
], ],
}) })
export class OrganizationsModule {} export class OrganizationsModule {}
+510 -224
View File
@@ -4384,6 +4384,292 @@
] ]
} }
}, },
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links": {
"post": {
"operationId": "OrganizationsEventTypesPrivateLinksController_createPrivateLink",
"summary": "Create a private link for a team event type",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreatePrivateLinkInput"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreatePrivateLinkOutput"
}
}
}
}
},
"tags": [
"Orgs / Teams / Event Types / Private Links"
]
},
"get": {
"operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks",
"summary": "Get all private links for a team event type",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetPrivateLinksOutput"
}
}
}
}
},
"tags": [
"Orgs / Teams / Event Types / Private Links"
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": {
"patch": {
"operationId": "OrganizationsEventTypesPrivateLinksController_updatePrivateLink",
"summary": "Update a private link for a team event type",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "linkId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdatePrivateLinkOutput"
}
}
}
}
},
"tags": [
"Orgs / Teams / Event Types / Private Links"
]
},
"delete": {
"operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink",
"summary": "Delete a private link for a team event type",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "linkId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletePrivateLinkOutput"
}
}
}
}
},
"tags": [
"Orgs / Teams / Event Types / Private Links"
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/memberships": { "/v2/organizations/{orgId}/teams/{teamId}/memberships": {
"get": { "get": {
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships", "operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
@@ -23789,6 +24075,213 @@
} }
} }
}, },
"CreatePrivateLinkInput": {
"type": "object",
"properties": {
"expiresAt": {
"type": "string",
"description": "Expiration date for time-based links",
"format": "date-time",
"example": "2024-12-31T23:59:59.000Z"
},
"maxUsageCount": {
"type": "number",
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
"example": 10,
"minimum": 1,
"default": 1
}
}
},
"TimeBasedPrivateLinkOutput": {
"type": "object",
"properties": {
"linkId": {
"type": "string",
"description": "The private link ID",
"example": "abc123def456"
},
"eventTypeId": {
"type": "number",
"description": "Event type ID this link belongs to",
"example": 123
},
"isExpired": {
"type": "boolean",
"description": "Whether the link is currently expired",
"example": false
},
"bookingUrl": {
"type": "string",
"description": "Full booking URL for this private link",
"format": "uri",
"example": "https://cal.com/d/abc123def456/30min"
},
"expiresAt": {
"type": "string",
"description": "Expiration date for this time-based link",
"format": "date-time",
"example": "2025-12-31T23:59:59.000Z"
}
},
"required": [
"linkId",
"eventTypeId",
"isExpired",
"bookingUrl",
"expiresAt"
]
},
"UsageBasedPrivateLinkOutput": {
"type": "object",
"properties": {
"linkId": {
"type": "string",
"description": "The private link ID",
"example": "abc123def456"
},
"eventTypeId": {
"type": "number",
"description": "Event type ID this link belongs to",
"example": 123
},
"isExpired": {
"type": "boolean",
"description": "Whether the link is currently expired",
"example": false
},
"bookingUrl": {
"type": "string",
"description": "Full booking URL for this private link",
"format": "uri",
"example": "https://cal.com/d/abc123def456/30min"
},
"maxUsageCount": {
"type": "number",
"description": "Maximum number of times this link can be used",
"example": 10
},
"usageCount": {
"type": "number",
"description": "Current usage count for this link",
"example": 3
}
},
"required": [
"linkId",
"eventTypeId",
"isExpired",
"bookingUrl",
"maxUsageCount",
"usageCount"
]
},
"CreatePrivateLinkOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Response status",
"example": "success"
},
"data": {
"description": "Created private link data (either time-based or usage-based)",
"oneOf": [
{
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
},
{
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
}
]
}
},
"required": [
"status",
"data"
]
},
"GetPrivateLinksOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Response status",
"example": "success"
},
"data": {
"type": "array",
"description": "Array of private links for the event type (mix of time-based and usage-based)",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
},
{
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
}
]
}
}
},
"required": [
"status",
"data"
]
},
"UpdatePrivateLinkOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Response status",
"example": "success"
},
"data": {
"description": "Updated private link data (either time-based or usage-based)",
"oneOf": [
{
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
},
{
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
}
]
}
},
"required": [
"status",
"data"
]
},
"DeletePrivateLinkOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Response status",
"example": "success"
},
"data": {
"type": "object",
"description": "Deleted link information",
"properties": {
"linkId": {
"type": "string",
"example": "abc123def456"
},
"message": {
"type": "string",
"example": "Private link deleted successfully"
}
}
}
},
"required": [
"status",
"data"
]
},
"StripConnectOutputDto": { "StripConnectOutputDto": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -25637,6 +26130,23 @@
"data" "data"
] ]
}, },
"UpdatePrivateLinkBody": {
"type": "object",
"properties": {
"expiresAt": {
"format": "date-time",
"type": "string",
"description": "New expiration date for time-based links",
"example": "2024-12-31T23:59:59.000Z"
},
"maxUsageCount": {
"type": "number",
"description": "New maximum number of times the link can be used",
"example": 10,
"minimum": 1
}
}
},
"MeOrgOutput": { "MeOrgOutput": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -28282,230 +28792,6 @@
"data" "data"
] ]
}, },
"CreatePrivateLinkInput": {
"type": "object",
"properties": {
"expiresAt": {
"type": "string",
"description": "Expiration date for time-based links",
"format": "date-time",
"example": "2024-12-31T23:59:59.000Z"
},
"maxUsageCount": {
"type": "number",
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
"example": 10,
"minimum": 1,
"default": 1
}
}
},
"TimeBasedPrivateLinkOutput": {
"type": "object",
"properties": {
"linkId": {
"type": "string",
"description": "The private link ID",
"example": "abc123def456"
},
"eventTypeId": {
"type": "number",
"description": "Event type ID this link belongs to",
"example": 123
},
"isExpired": {
"type": "boolean",
"description": "Whether the link is currently expired",
"example": false
},
"bookingUrl": {
"type": "string",
"description": "Full booking URL for this private link",
"format": "uri",
"example": "https://cal.com/d/abc123def456/30min"
},
"expiresAt": {
"type": "string",
"description": "Expiration date for this time-based link",
"format": "date-time",
"example": "2025-12-31T23:59:59.000Z"
}
},
"required": [
"linkId",
"eventTypeId",
"isExpired",
"bookingUrl",
"expiresAt"
]
},
"UsageBasedPrivateLinkOutput": {
"type": "object",
"properties": {
"linkId": {
"type": "string",
"description": "The private link ID",
"example": "abc123def456"
},
"eventTypeId": {
"type": "number",
"description": "Event type ID this link belongs to",
"example": 123
},
"isExpired": {
"type": "boolean",
"description": "Whether the link is currently expired",
"example": false
},
"bookingUrl": {
"type": "string",
"description": "Full booking URL for this private link",
"format": "uri",
"example": "https://cal.com/d/abc123def456/30min"
},
"maxUsageCount": {
"type": "number",
"description": "Maximum number of times this link can be used",
"example": 10
},
"usageCount": {
"type": "number",
"description": "Current usage count for this link",
"example": 3
}
},
"required": [
"linkId",
"eventTypeId",
"isExpired",
"bookingUrl",
"maxUsageCount",
"usageCount"
]
},
"CreatePrivateLinkOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Response status",
"example": "success"
},
"data": {
"description": "Created private link data (either time-based or usage-based)",
"oneOf": [
{
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
},
{
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
}
]
}
},
"required": [
"status",
"data"
]
},
"GetPrivateLinksOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Response status",
"example": "success"
},
"data": {
"type": "array",
"description": "Array of private links for the event type (mix of time-based and usage-based)",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
},
{
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
}
]
}
}
},
"required": [
"status",
"data"
]
},
"UpdatePrivateLinkBody": {
"type": "object",
"properties": {
"expiresAt": {
"format": "date-time",
"type": "string",
"description": "New expiration date for time-based links",
"example": "2024-12-31T23:59:59.000Z"
},
"maxUsageCount": {
"type": "number",
"description": "New maximum number of times the link can be used",
"example": 10,
"minimum": 1
}
}
},
"UpdatePrivateLinkOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Response status",
"example": "success"
},
"data": {
"description": "Updated private link data (either time-based or usage-based)",
"oneOf": [
{
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
},
{
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
}
]
}
},
"required": [
"status",
"data"
]
},
"DeletePrivateLinkOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Response status",
"example": "success"
},
"data": {
"type": "object",
"description": "Deleted link information",
"properties": {
"linkId": {
"type": "string",
"example": "abc123def456"
},
"message": {
"type": "string",
"example": "Private link deleted successfully"
}
}
}
},
"required": [
"status",
"data"
]
},
"UserWebhookOutputDto": { "UserWebhookOutputDto": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -34,4 +34,13 @@ export class ManagedOrganizationsRepositoryFixture {
}, },
}); });
} }
async createManagedOrganization(managerOrganizationId: number, managedOrganizationId: number) {
return this.prismaWriteClient.managedOrganization.create({
data: {
managerOrganizationId,
managedOrganizationId,
},
});
}
} }
+2 -2
View File
@@ -13,5 +13,5 @@ export type PageProps = {
export type LayoutProps = { params: Promise<Params>; children: React.ReactElement }; export type LayoutProps = { params: Promise<Params>; children: React.ReactElement };
export { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; export type ReadonlyHeaders = Awaited<ReturnType<typeof import("next/headers").headers>>;
export { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies"; export type ReadonlyRequestCookies = Awaited<ReturnType<typeof import("next/headers").cookies>>;
+1 -1
View File
@@ -200,7 +200,7 @@ async function getHandler(request: NextRequest) {
buffer = await optimizeImage({ buffer = await optimizeImage({
buffer, buffer,
contentType: detectContentType(buffer) ?? "image/jpeg", contentType: (await detectContentType(buffer)) ?? "image/jpeg",
quality: 100, quality: 100,
width: logoDefinition.w, width: logoDefinition.w,
height: logoDefinition.h, // optional height: logoDefinition.h, // optional
+1 -2
View File
@@ -1,8 +1,7 @@
import type { SearchParams } from "app/_types"; import type { SearchParams } from "app/_types";
import { type Params } from "app/_types"; import { type Params } from "app/_types";
import type { ReadonlyHeaders, ReadonlyRequestCookies } from "app/_types";
import type { GetServerSidePropsContext, NextApiRequest } from "next"; import type { GetServerSidePropsContext, NextApiRequest } from "next";
import { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
import { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
const createProxifiedObject = (object: Record<string, string>) => const createProxifiedObject = (object: Record<string, string>) =>
new Proxy(object, { new Proxy(object, {
+1 -1
View File
@@ -106,7 +106,7 @@
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"micro": "^10.0.1", "micro": "^10.0.1",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"next": "^15.3.0", "next": "^15.4.5",
"next-auth": "^4.22.1", "next-auth": "^4.22.1",
"next-axiom": "^0.17.0", "next-axiom": "^0.17.0",
"next-collect": "^0.2.1", "next-collect": "^0.2.1",
+3 -3
View File
@@ -1,6 +1,6 @@
import type { IncomingMessage } from "http"; import type { IncomingMessage } from "http";
import type { NextPageContext } from "next";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
import type { AppContextType } from "next/dist/shared/lib/utils";
import React from "react"; import React from "react";
import CacheProvider from "react-inlinesvg/provider"; import CacheProvider from "react-inlinesvg/provider";
@@ -32,8 +32,8 @@ declare global {
} }
} }
MyApp.getInitialProps = async (ctx: AppContextType) => { MyApp.getInitialProps = async ({ ctx }: { ctx: NextPageContext }) => {
const { req } = ctx.ctx; const { req } = ctx;
let newLocale = "en"; let newLocale = "en";
+510
View File
@@ -4193,6 +4193,292 @@
"tags": ["Orgs / Teams / Event Types"] "tags": ["Orgs / Teams / Event Types"]
} }
}, },
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links": {
"post": {
"operationId": "OrganizationsEventTypesPrivateLinksController_createPrivateLink",
"summary": "Create a private link for a team event type",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreatePrivateLinkInput"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreatePrivateLinkOutput"
}
}
}
}
},
"tags": [
"Orgs / Teams / Event Types / Private Links"
]
},
"get": {
"operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks",
"summary": "Get all private links for a team event type",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetPrivateLinksOutput"
}
}
}
}
},
"tags": [
"Orgs / Teams / Event Types / Private Links"
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": {
"patch": {
"operationId": "OrganizationsEventTypesPrivateLinksController_updatePrivateLink",
"summary": "Update a private link for a team event type",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "linkId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdatePrivateLinkOutput"
}
}
}
}
},
"tags": [
"Orgs / Teams / Event Types / Private Links"
]
},
"delete": {
"operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink",
"summary": "Delete a private link for a team event type",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "linkId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletePrivateLinkOutput"
}
}
}
}
},
"tags": [
"Orgs / Teams / Event Types / Private Links"
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/memberships": { "/v2/organizations/{orgId}/teams/{teamId}/memberships": {
"get": { "get": {
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships", "operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
@@ -21786,6 +22072,213 @@
} }
} }
}, },
"CreatePrivateLinkInput": {
"type": "object",
"properties": {
"expiresAt": {
"type": "string",
"description": "Expiration date for time-based links",
"format": "date-time",
"example": "2024-12-31T23:59:59.000Z"
},
"maxUsageCount": {
"type": "number",
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
"example": 10,
"minimum": 1,
"default": 1
}
}
},
"TimeBasedPrivateLinkOutput": {
"type": "object",
"properties": {
"linkId": {
"type": "string",
"description": "The private link ID",
"example": "abc123def456"
},
"eventTypeId": {
"type": "number",
"description": "Event type ID this link belongs to",
"example": 123
},
"isExpired": {
"type": "boolean",
"description": "Whether the link is currently expired",
"example": false
},
"bookingUrl": {
"type": "string",
"description": "Full booking URL for this private link",
"format": "uri",
"example": "https://cal.com/d/abc123def456/30min"
},
"expiresAt": {
"type": "string",
"description": "Expiration date for this time-based link",
"format": "date-time",
"example": "2025-12-31T23:59:59.000Z"
}
},
"required": [
"linkId",
"eventTypeId",
"isExpired",
"bookingUrl",
"expiresAt"
]
},
"UsageBasedPrivateLinkOutput": {
"type": "object",
"properties": {
"linkId": {
"type": "string",
"description": "The private link ID",
"example": "abc123def456"
},
"eventTypeId": {
"type": "number",
"description": "Event type ID this link belongs to",
"example": 123
},
"isExpired": {
"type": "boolean",
"description": "Whether the link is currently expired",
"example": false
},
"bookingUrl": {
"type": "string",
"description": "Full booking URL for this private link",
"format": "uri",
"example": "https://cal.com/d/abc123def456/30min"
},
"maxUsageCount": {
"type": "number",
"description": "Maximum number of times this link can be used",
"example": 10
},
"usageCount": {
"type": "number",
"description": "Current usage count for this link",
"example": 3
}
},
"required": [
"linkId",
"eventTypeId",
"isExpired",
"bookingUrl",
"maxUsageCount",
"usageCount"
]
},
"CreatePrivateLinkOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Response status",
"example": "success"
},
"data": {
"description": "Created private link data (either time-based or usage-based)",
"oneOf": [
{
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
},
{
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
}
]
}
},
"required": [
"status",
"data"
]
},
"GetPrivateLinksOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Response status",
"example": "success"
},
"data": {
"type": "array",
"description": "Array of private links for the event type (mix of time-based and usage-based)",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
},
{
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
}
]
}
}
},
"required": [
"status",
"data"
]
},
"UpdatePrivateLinkOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Response status",
"example": "success"
},
"data": {
"description": "Updated private link data (either time-based or usage-based)",
"oneOf": [
{
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
},
{
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
}
]
}
},
"required": [
"status",
"data"
]
},
"DeletePrivateLinkOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Response status",
"example": "success"
},
"data": {
"type": "object",
"description": "Deleted link information",
"properties": {
"linkId": {
"type": "string",
"example": "abc123def456"
},
"message": {
"type": "string",
"example": "Private link deleted successfully"
}
}
}
},
"required": [
"status",
"data"
]
},
"StripConnectOutputDto": { "StripConnectOutputDto": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -23368,6 +23861,23 @@
}, },
"required": ["status", "data"] "required": ["status", "data"]
}, },
"UpdatePrivateLinkBody": {
"type": "object",
"properties": {
"expiresAt": {
"format": "date-time",
"type": "string",
"description": "New expiration date for time-based links",
"example": "2024-12-31T23:59:59.000Z"
},
"maxUsageCount": {
"type": "number",
"description": "New maximum number of times the link can be used",
"example": 10,
"minimum": 1
}
}
},
"MeOrgOutput": { "MeOrgOutput": {
"type": "object", "type": "object",
"properties": { "properties": {
+3 -2
View File
@@ -2,11 +2,12 @@ import { parse } from "accept-language-parser";
import { lookup } from "bcp-47-match"; import { lookup } from "bcp-47-match";
import type { GetTokenParams } from "next-auth/jwt"; import type { GetTokenParams } from "next-auth/jwt";
import { getToken } from "next-auth/jwt"; import { getToken } from "next-auth/jwt";
import { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
import { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
import { i18n } from "@calcom/config/next-i18next.config"; import { i18n } from "@calcom/config/next-i18next.config";
type ReadonlyHeaders = Awaited<ReturnType<typeof import("next/headers").headers>>;
type ReadonlyRequestCookies = Awaited<ReturnType<typeof import("next/headers").cookies>>;
/** /**
* This is a slimmed down version of the `getServerSession` function from * This is a slimmed down version of the `getServerSession` function from
* `next-auth`. * `next-auth`.
+20 -9
View File
@@ -10,6 +10,7 @@ import dayjs from "@calcom/dayjs";
import PoweredBy from "@calcom/ee/components/PoweredBy"; import PoweredBy from "@calcom/ee/components/PoweredBy";
import { updateEmbedBookerState } from "@calcom/embed-core/src/embed-iframe"; import { updateEmbedBookerState } from "@calcom/embed-core/src/embed-iframe";
import TurnstileCaptcha from "@calcom/features/auth/Turnstile"; import TurnstileCaptcha from "@calcom/features/auth/Turnstile";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import useSkipConfirmStep from "@calcom/features/bookings/Booker/components/hooks/useSkipConfirmStep"; import useSkipConfirmStep from "@calcom/features/bookings/Booker/components/hooks/useSkipConfirmStep";
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param"; import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays"; import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
@@ -39,7 +40,6 @@ import { NotFound } from "./components/Unavailable";
import { useIsQuickAvailabilityCheckFeatureEnabled } from "./components/hooks/useIsQuickAvailabilityCheckFeatureEnabled"; import { useIsQuickAvailabilityCheckFeatureEnabled } from "./components/hooks/useIsQuickAvailabilityCheckFeatureEnabled";
import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config"; import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
import framerFeatures from "./framer-features"; import framerFeatures from "./framer-features";
import { useBookerStore } from "./store";
import type { BookerProps, WrappedBookerProps } from "./types"; import type { BookerProps, WrappedBookerProps } from "./types";
import { isBookingDryRun } from "./utils/isBookingDryRun"; import { isBookingDryRun } from "./utils/isBookingDryRun";
import { isTimeSlotAvailable } from "./utils/isTimeslotAvailable"; import { isTimeSlotAvailable } from "./utils/isTimeslotAvailable";
@@ -85,9 +85,14 @@ const BookerComponent = ({
}: BookerProps & WrappedBookerProps) => { }: BookerProps & WrappedBookerProps) => {
const searchParams = useCompatSearchParams(); const searchParams = useCompatSearchParams();
const isPlatformBookerEmbed = useIsPlatformBookerEmbed(); const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow); const [bookerState, setBookerState] = useBookerStoreContext(
(state) => [state.state, state.setState],
shallow
);
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
const selectedDate = useBookerStore((state) => state.selectedDate);
const { const {
shouldShowFormInDialog, shouldShowFormInDialog,
hasDarkBackground, hasDarkBackground,
@@ -100,12 +105,15 @@ const BookerComponent = ({
bookerLayouts, bookerLayouts,
} = bookerLayout; } = bookerLayout;
const [seatedEventData, setSeatedEventData] = useBookerStore( const [seatedEventData, setSeatedEventData] = useBookerStoreContext(
(state) => [state.seatedEventData, state.setSeatedEventData], (state) => [state.seatedEventData, state.setSeatedEventData],
shallow shallow
); );
const { selectedTimeslot, setSelectedTimeslot, allSelectedTimeslots } = slots; const { selectedTimeslot, setSelectedTimeslot, allSelectedTimeslots } = slots;
const [dayCount, setDayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow); const [dayCount, setDayCount] = useBookerStoreContext(
(state) => [state.dayCount, state.setDayCount],
shallow
);
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter( const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter(
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0 (slot) => dayjs(selectedDate).diff(slot, "day") <= 0
@@ -184,6 +192,10 @@ const BookerComponent = ({
(bookerState === "booking" || (bookerState === "selecting_time" && skipConfirmStep)) (bookerState === "booking" || (bookerState === "selecting_time" && skipConfirmStep))
); );
const onAvailableTimeSlotSelect = (time: string) => {
setSelectedTimeslot(time);
};
updateEmbedBookerState({ bookerState, slotsQuery: schedule }); updateEmbedBookerState({ bookerState, slotsQuery: schedule });
useEffect(() => { useEffect(() => {
@@ -220,6 +232,7 @@ const BookerComponent = ({
return bookerState === "booking" ? ( return bookerState === "booking" ? (
<BookEventForm <BookEventForm
key={key} key={key}
timeslot={selectedTimeslot}
shouldRenderCaptcha={shouldRenderCaptcha} shouldRenderCaptcha={shouldRenderCaptcha}
onCancel={() => { onCancel={() => {
setSelectedTimeslot(null); setSelectedTimeslot(null);
@@ -312,9 +325,7 @@ const BookerComponent = ({
return ( return (
<> <>
{event.data && !isPlatform ? <BookingPageTagManager eventType={event.data} /> : <></>} {event.data && !isPlatform ? <BookingPageTagManager eventType={event.data} /> : <></>}
{(isBookingDryRunProp || isBookingDryRun(searchParams)) && <DryRunMessage isEmbed={isEmbed} />} {(isBookingDryRunProp || isBookingDryRun(searchParams)) && <DryRunMessage isEmbed={isEmbed} />}
<div <div
className={classNames( className={classNames(
// In a popup embed, if someone clicks outside the main(having main class or main tag), it closes the embed // In a popup embed, if someone clicks outside the main(having main class or main tag), it closes the embed
@@ -394,6 +405,7 @@ const BookerComponent = ({
/> />
)} )}
<EventMeta <EventMeta
selectedTimeslot={selectedTimeslot}
classNames={{ classNames={{
eventMetaContainer: customClassNames?.eventMetaCustomClassNames?.eventMetaContainer, eventMetaContainer: customClassNames?.eventMetaCustomClassNames?.eventMetaContainer,
eventMetaTitle: customClassNames?.eventMetaCustomClassNames?.eventMetaTitle, eventMetaTitle: customClassNames?.eventMetaCustomClassNames?.eventMetaTitle,
@@ -480,6 +492,7 @@ const BookerComponent = ({
ref={timeslotsRef} ref={timeslotsRef}
{...fadeInLeft}> {...fadeInLeft}>
<AvailableTimeSlots <AvailableTimeSlots
onAvailableTimeSlotSelect={onAvailableTimeSlotSelect}
customClassNames={customClassNames?.availableTimeSlotsCustomClassNames} customClassNames={customClassNames?.availableTimeSlotsCustomClassNames}
extraDays={extraDays} extraDays={extraDays}
limitHeight={layout === BookerLayouts.MONTH_VIEW} limitHeight={layout === BookerLayouts.MONTH_VIEW}
@@ -552,7 +565,6 @@ const BookerComponent = ({
</m.span> </m.span>
)} )}
</div> </div>
<> <>
{verifyCode && formEmail ? ( {verifyCode && formEmail ? (
<VerifyCodeDialog <VerifyCodeDialog
@@ -571,7 +583,6 @@ const BookerComponent = ({
<></> <></>
)} )}
</> </>
<BookFormAsModal <BookFormAsModal
onCancel={() => setSelectedTimeslot(null)} onCancel={() => setSelectedTimeslot(null)}
visible={bookerState === "booking" && shouldShowFormInDialog}> visible={bookerState === "booking" && shouldShowFormInDialog}>
@@ -0,0 +1,113 @@
"use client";
import { createContext, useContext, useRef, type ReactNode, useEffect } from "react";
import { useStore } from "zustand";
import type { StoreApi } from "zustand";
import { createBookerStore, type BookerStore, type StoreInitializeType } from "./store";
export const BookerStoreContext = createContext<StoreApi<BookerStore> | null>(null);
export interface BookerStoreProviderProps {
children: ReactNode;
}
export const BookerStoreProvider = ({ children }: BookerStoreProviderProps) => {
const storeRef = useRef<StoreApi<BookerStore>>();
if (!storeRef.current) {
storeRef.current = createBookerStore();
}
return <BookerStoreContext.Provider value={storeRef.current}>{children}</BookerStoreContext.Provider>;
};
export const useBookerStoreContext = <T,>(
selector: (store: BookerStore) => T,
equalityFn?: (a: T, b: T) => boolean
): T => {
const bookerStoreContext = useContext(BookerStoreContext);
if (!bookerStoreContext) {
throw new Error("useBookerStoreContext must be used within BookerStoreProvider");
}
return useStore(bookerStoreContext, selector, equalityFn);
};
export const useInitializeBookerStoreContext = ({
username,
eventSlug,
month,
eventId,
rescheduleUid = null,
rescheduledBy = null,
bookingData = null,
verifiedEmail = null,
layout,
isTeamEvent,
durationConfig,
org,
isInstantMeeting,
timezone = null,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform = false,
allowUpdatingUrlParams = true,
}: StoreInitializeType) => {
const bookerStoreContext = useContext(BookerStoreContext);
if (!bookerStoreContext) {
throw new Error("useInitializeBookerStoreContext must be used within BookerStoreProvider");
}
const initializeStore = useStore(bookerStoreContext, (state) => state.initialize);
useEffect(() => {
initializeStore({
username,
eventSlug,
month,
eventId,
rescheduleUid,
rescheduledBy,
bookingData,
layout,
isTeamEvent,
org,
verifiedEmail,
durationConfig,
isInstantMeeting,
timezone,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform,
allowUpdatingUrlParams,
});
}, [
initializeStore,
org,
username,
eventSlug,
month,
eventId,
rescheduleUid,
rescheduledBy,
bookingData,
layout,
isTeamEvent,
verifiedEmail,
durationConfig,
isInstantMeeting,
timezone,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform,
allowUpdatingUrlParams,
]);
};
@@ -10,18 +10,17 @@ import "@calcom/features/bookings/Booker/components/__mocks__/Section";
import { constantsScenarios } from "@calcom/lib/__mocks__/constants"; import { constantsScenarios } from "@calcom/lib/__mocks__/constants";
import "@calcom/lib/__mocks__/logger"; import "@calcom/lib/__mocks__/logger";
import { render, screen } from "@testing-library/react"; import React from "react";
import { vi } from "vitest"; import { vi } from "vitest";
import "@calcom/dayjs/__mocks__"; import "@calcom/dayjs/__mocks__";
import "@calcom/features/auth/Turnstile"; import "@calcom/features/auth/Turnstile";
import { Booker } from "../Booker"; import { Booker } from "../Booker";
import { useBookerStore } from "../store"; import { render, screen } from "./test-utils";
import type { BookerState } from "../types";
vi.mock("framer-motion", async (importOriginal) => { vi.mock("framer-motion", async (importOriginal) => {
const actual = await importOriginal(); const actual = (await importOriginal()) as any;
return { return {
...actual, ...actual,
}; };
@@ -87,27 +86,6 @@ vi.mock("@calcom/atoms/hooks/useIsPlatform", () => ({
useIsPlatform: () => false, useIsPlatform: () => false,
})); }));
// Update mockStoreState to include all required state
const mockStoreState = {
state: "booking" as BookerState,
setState: vi.fn(),
selectedDate: "2024-01-01",
seatedEventData: {},
setSeatedEventData: vi.fn(),
tentativeSelectedTimeslots: [],
setTentativeSelectedTimeslots: vi.fn(),
dayCount: 7,
setDayCount: vi.fn(),
setSelectedTimeslot: vi.fn(),
selectedTimeslot: null,
formStep: 0,
setFormStep: vi.fn(),
bookerState: "booking",
setBookerState: vi.fn(),
layout: "default",
setLayout: vi.fn(),
};
// Update defaultProps to include missing required props // Update defaultProps to include missing required props
const defaultProps = { const defaultProps = {
username: "testuser", username: "testuser",
@@ -188,7 +166,7 @@ const defaultProps = {
describe("Booker", () => { describe("Booker", () => {
beforeEach(() => { beforeEach(() => {
constantsScenarios.set({ constantsScenarios.set({
PUBLIC_QUICK_AVAILABILITY_ROLLOUT: 100, PUBLIC_QUICK_AVAILABILITY_ROLLOUT: "100",
POWERED_BY_URL: "https://go.cal.com/booking", POWERED_BY_URL: "https://go.cal.com/booking",
APP_NAME: "Cal.com", APP_NAME: "Cal.com",
}); });
@@ -196,24 +174,13 @@ describe("Booker", () => {
}); });
it("should render null when in loading state", () => { it("should render null when in loading state", () => {
useBookerStore.setState({ const { container } = render(<Booker {...defaultProps} />, {
...mockStoreState, mockStore: { state: "loading" },
state: "loading",
}); });
const { container } = render(<Booker {...defaultProps} />);
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
}); });
it("should render DryRunMessage when in dry run mode", () => { it("should render DryRunMessage when in dry run mode", () => {
useBookerStore.setState({
...mockStoreState,
state: "selecting_time",
selectedDate: "2024-01-01",
selectedTimeslot: "2024-01-01T10:00:00Z",
tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
});
const propsWithDryRun = { const propsWithDryRun = {
...defaultProps, ...defaultProps,
isBookingDryRun: true, isBookingDryRun: true,
@@ -226,7 +193,14 @@ describe("Booker", () => {
}, },
}; };
render(<Booker {...propsWithDryRun} />); render(<Booker {...propsWithDryRun} />, {
mockStore: {
state: "selecting_time",
selectedDate: "2024-01-01",
selectedTimeslot: "2024-01-01T10:00:00Z",
tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
},
});
expect(screen.getByTestId("dry-run-message")).toBeInTheDocument(); expect(screen.getByTestId("dry-run-message")).toBeInTheDocument();
}); });
@@ -242,12 +216,10 @@ describe("Booker", () => {
invalidate: mockInvalidate, invalidate: mockInvalidate,
}, },
}; };
useBookerStore.setState({
...mockStoreState,
state: "booking",
});
render(<Booker {...propsWithInvalidate} />); render(<Booker {...propsWithInvalidate} />, {
mockStore: { state: "booking" },
});
screen.logTestingPlaygroundURL(); screen.logTestingPlaygroundURL();
// Trigger form cancel // Trigger form cancel
const cancelButton = screen.getByRole("button", { name: /cancel/i }); const cancelButton = screen.getByRole("button", { name: /cancel/i });
@@ -267,12 +239,9 @@ describe("Booker", () => {
}, },
}; };
useBookerStore.setState({ render(<Booker {...propsWithQuickChecks} />, {
...mockStoreState, mockStore: { state: "booking" },
state: "booking",
}); });
render(<Booker {...propsWithQuickChecks} />);
const bookEventForm = screen.getByTestId("book-event-form"); const bookEventForm = screen.getByTestId("book-event-form");
await expect(bookEventForm).toHaveAttribute("data-unavailable", "true"); await expect(bookEventForm).toHaveAttribute("data-unavailable", "true");
}); });
@@ -0,0 +1,105 @@
import { render } from "@testing-library/react";
import type { RenderOptions } from "@testing-library/react";
import React from "react";
import type { ReactElement } from "react";
import { vi } from "vitest";
import type { StoreApi } from "zustand";
import dayjs from "@calcom/dayjs";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { BookerStoreContext } from "../BookerStoreProvider";
import type { BookerStore } from "../store";
interface CustomRenderOptions extends Omit<RenderOptions, "wrapper"> {
mockStore?: Partial<BookerStore>;
}
const createMockStore = (initialState?: Partial<BookerStore>): StoreApi<BookerStore> => {
let state: BookerStore = {
username: null,
eventSlug: null,
eventId: null,
verifiedEmail: null,
setVerifiedEmail: vi.fn(),
month: dayjs().format("YYYY-MM"),
setMonth: vi.fn(),
state: "loading",
setState: vi.fn(),
layout: BookerLayouts.MONTH_VIEW,
setLayout: vi.fn(),
selectedDate: null,
setSelectedDate: vi.fn(),
addToSelectedDate: vi.fn(),
selectedDatesAndTimes: null,
setSelectedDatesAndTimes: vi.fn(),
durationConfig: null,
selectedDuration: null,
setSelectedDuration: vi.fn(),
selectedTimeslot: null,
setSelectedTimeslot: vi.fn(),
tentativeSelectedTimeslots: [],
setTentativeSelectedTimeslots: vi.fn(),
recurringEventCount: null,
setRecurringEventCount: vi.fn(),
occurenceCount: null,
setOccurenceCount: vi.fn(),
dayCount: null,
setDayCount: vi.fn(),
rescheduleUid: null,
rescheduledBy: null,
bookingUid: null,
bookingData: null,
setBookingData: vi.fn(),
initialize: vi.fn(),
formValues: {},
setFormValues: vi.fn(),
isTeamEvent: false,
seatedEventData: {},
setSeatedEventData: vi.fn(),
isInstantMeeting: false,
org: null,
setOrg: vi.fn(),
timezone: null,
setTimezone: vi.fn(),
teamMemberEmail: null,
crmOwnerRecordType: null,
crmAppSlug: null,
crmRecordId: null,
isPlatform: false,
allowUpdatingUrlParams: true,
...initialState,
};
state.setMonth = vi.fn((month: string | null) => {
state.month = month;
});
return {
getState: () => state,
setState: vi.fn((updater) => {
if (typeof updater === "function") {
const newState = updater(state);
state = { ...state, ...newState };
} else {
state = { ...state, ...updater };
}
}),
subscribe: vi.fn(),
destroy: vi.fn(),
} as unknown as StoreApi<BookerStore>;
};
export const renderWithBookerStore = (ui: ReactElement, options?: CustomRenderOptions) => {
const mockStore = createMockStore(options?.mockStore);
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<BookerStoreContext.Provider value={mockStore}>{children}</BookerStoreContext.Provider>
);
return render(ui, { wrapper: Wrapper, ...options });
};
export * from "@testing-library/react";
export { renderWithBookerStore as render };
@@ -2,6 +2,7 @@ import { useCallback, useMemo, useRef } from "react";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings"; import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings"; import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
import type { BookerEvent } from "@calcom/features/bookings/types"; import type { BookerEvent } from "@calcom/features/bookings/types";
import type { Slot } from "@calcom/features/schedules/lib/use-schedule/types"; import type { Slot } from "@calcom/features/schedules/lib/use-schedule/types";
@@ -13,7 +14,6 @@ import { BookerLayouts } from "@calcom/prisma/zod-utils";
import classNames from "@calcom/ui/classNames"; import classNames from "@calcom/ui/classNames";
import { AvailableTimesHeader } from "../../components/AvailableTimesHeader"; import { AvailableTimesHeader } from "../../components/AvailableTimesHeader";
import { useBookerStore } from "../store";
import type { useScheduleForEventReturnType } from "../utils/event"; import type { useScheduleForEventReturnType } from "../utils/event";
import { getQueryParam } from "../utils/query-param"; import { getQueryParam } from "../utils/query-param";
@@ -49,6 +49,7 @@ type AvailableTimeSlotsProps = {
*/ */
unavailableTimeSlots: string[]; unavailableTimeSlots: string[];
confirmButtonDisabled?: boolean; confirmButtonDisabled?: boolean;
onAvailableTimeSlotSelect: (time: string) => void;
}; };
/** /**
@@ -72,17 +73,17 @@ export const AvailableTimeSlots = ({
unavailableTimeSlots, unavailableTimeSlots,
confirmButtonDisabled, confirmButtonDisabled,
confirmStepClassNames, confirmStepClassNames,
onAvailableTimeSlotSelect,
...props ...props
}: AvailableTimeSlotsProps) => { }: AvailableTimeSlotsProps) => {
const selectedDate = useBookerStore((state) => state.selectedDate); const selectedDate = useBookerStoreContext((state) => state.selectedDate);
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot); const setSeatedEventData = useBookerStoreContext((state) => state.setSeatedEventData);
const setSeatedEventData = useBookerStore((state) => state.setSeatedEventData);
const date = selectedDate || dayjs().format("YYYY-MM-DD"); const date = selectedDate || dayjs().format("YYYY-MM-DD");
const [layout] = useBookerStore((state) => [state.layout]); const [layout] = useBookerStoreContext((state) => [state.layout]);
const isColumnView = layout === BookerLayouts.COLUMN_VIEW; const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const { setTentativeSelectedTimeslots, tentativeSelectedTimeslots } = useBookerStore((state) => ({ const { setTentativeSelectedTimeslots, tentativeSelectedTimeslots } = useBookerStoreContext((state) => ({
setTentativeSelectedTimeslots: state.setTentativeSelectedTimeslots, setTentativeSelectedTimeslots: state.setTentativeSelectedTimeslots,
tentativeSelectedTimeslots: state.tentativeSelectedTimeslots, tentativeSelectedTimeslots: state.tentativeSelectedTimeslots,
})); }));
@@ -146,7 +147,9 @@ export const AvailableTimeSlots = ({
showAvailableSeatsCount, showAvailableSeatsCount,
}); });
} }
setSelectedTimeslot(time);
onAvailableTimeSlotSelect(time);
const isTimeSlotAvailable = !unavailableTimeSlots.includes(time); const isTimeSlotAvailable = !unavailableTimeSlots.includes(time);
if (skipConfirmStep && isTimeSlotAvailable) { if (skipConfirmStep && isTimeSlotAvailable) {
onSubmit(time); onSubmit(time);
@@ -156,12 +159,12 @@ export const AvailableTimeSlots = ({
[ [
onSubmit, onSubmit,
setSeatedEventData, setSeatedEventData,
setSelectedTimeslot,
skipConfirmStep, skipConfirmStep,
showAvailableSeatsCount, showAvailableSeatsCount,
unavailableTimeSlots, unavailableTimeSlots,
schedule, schedule,
setTentativeSelectedTimeslots, setTentativeSelectedTimeslots,
onAvailableTimeSlotSelect,
] ]
); );
@@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
import type { FieldError } from "react-hook-form"; import type { FieldError } from "react-hook-form";
import { useIsPlatformBookerEmbed } from "@calcom/atoms/hooks/useIsPlatformBookerEmbed"; import { useIsPlatformBookerEmbed } from "@calcom/atoms/hooks/useIsPlatformBookerEmbed";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { BookerEvent } from "@calcom/features/bookings/types"; import type { BookerEvent } from "@calcom/features/bookings/types";
import ServerTrans from "@calcom/lib/components/ServerTrans"; import ServerTrans from "@calcom/lib/components/ServerTrans";
import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants"; import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants";
@@ -16,7 +17,6 @@ import { Button } from "@calcom/ui/components/button";
import { EmptyScreen } from "@calcom/ui/components/empty-screen"; import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { Form } from "@calcom/ui/components/form"; import { Form } from "@calcom/ui/components/form";
import { useBookerStore } from "../../store";
import { formatEventFromTime } from "../../utils/dates"; import { formatEventFromTime } from "../../utils/dates";
import { useBookerTime } from "../hooks/useBookerTime"; import { useBookerTime } from "../hooks/useBookerTime";
import type { UseBookingFormReturnType } from "../hooks/useBookingForm"; import type { UseBookingFormReturnType } from "../hooks/useBookingForm";
@@ -43,6 +43,7 @@ type BookEventFormProps = {
confirmButton?: string; confirmButton?: string;
backButton?: string; backButton?: string;
}; };
timeslot: string | null;
}; };
export const BookEventForm = ({ export const BookEventForm = ({
@@ -62,6 +63,7 @@ export const BookEventForm = ({
shouldRenderCaptcha, shouldRenderCaptcha,
confirmButtonDisabled, confirmButtonDisabled,
classNames, classNames,
timeslot,
}: Omit<BookEventFormProps, "event"> & { }: Omit<BookEventFormProps, "event"> & {
eventQuery: { eventQuery: {
isError: boolean; isError: boolean;
@@ -70,12 +72,11 @@ export const BookEventForm = ({
}; };
}) => { }) => {
const eventType = eventQuery.data; const eventType = eventQuery.data;
const setFormValues = useBookerStore((state) => state.setFormValues); const setFormValues = useBookerStoreContext((state) => state.setFormValues);
const bookingData = useBookerStore((state) => state.bookingData); const bookingData = useBookerStoreContext((state) => state.bookingData);
const rescheduleUid = useBookerStore((state) => state.rescheduleUid); const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
const timeslot = useBookerStore((state) => state.selectedTimeslot); const username = useBookerStoreContext((state) => state.username);
const username = useBookerStore((state) => state.username); const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
const isPlatformBookerEmbed = useIsPlatformBookerEmbed(); const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
const { timeFormat, timezone } = useBookerTime(); const { timeFormat, timezone } = useBookerTime();
@@ -3,13 +3,13 @@ import React from "react";
import { useEventTypeById } from "@calcom/atoms/hooks/event-types/private/useEventTypeById"; import { useEventTypeById } from "@calcom/atoms/hooks/event-types/private/useEventTypeById";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { Dialog } from "@calcom/features/components/controlled-dialog"; import { Dialog } from "@calcom/features/components/controlled-dialog";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge } from "@calcom/ui/components/badge"; import { Badge } from "@calcom/ui/components/badge";
import { DialogContent } from "@calcom/ui/components/dialog"; import { DialogContent } from "@calcom/ui/components/dialog";
import { getDurationFormatted } from "../../../components/event-meta/Duration"; import { getDurationFormatted } from "../../../components/event-meta/Duration";
import { useBookerStore } from "../../store";
import { FromTime } from "../../utils/dates"; import { FromTime } from "../../utils/dates";
import { useEvent } from "../../utils/event"; import { useEvent } from "../../utils/event";
import { useBookerTime } from "../hooks/useBookerTime"; import { useBookerTime } from "../hooks/useBookerTime";
@@ -27,7 +27,7 @@ const PlatformBookEventFormWrapper = ({
onCancel: () => void; onCancel: () => void;
children: ReactNode; children: ReactNode;
}) => { }) => {
const eventId = useBookerStore((state) => state.eventId); const eventId = useBookerStoreContext((state) => state.eventId);
const { data } = useEventTypeById(eventId); const { data } = useEventTypeById(eventId);
return ( return (
@@ -44,8 +44,8 @@ export const BookEventFormWrapperComponent = ({
eventLength?: number; eventLength?: number;
}) => { }) => {
const { i18n, t } = useLocale(); const { i18n, t } = useLocale();
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot); const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
const selectedDuration = useBookerStore((state) => state.selectedDuration); const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
const { timeFormat, timezone } = useBookerTime(); const { timeFormat, timezone } = useBookerTime();
if (!selectedTimeslot) { if (!selectedTimeslot) {
return null; return null;
@@ -1,9 +1,10 @@
import { render } from "@testing-library/react"; import React from "react";
import { vi, afterEach } from "vitest"; import { vi, afterEach } from "vitest";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker"; import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
import { render } from "../__tests__/test-utils";
import { DatePicker } from "./DatePicker"; import { DatePicker } from "./DatePicker";
vi.mock("@calcom/features/calendars/DatePicker", () => { vi.mock("@calcom/features/calendars/DatePicker", () => {
@@ -18,7 +19,7 @@ const noop = () => {
describe("Tests for DatePicker Component", () => { describe("Tests for DatePicker Component", () => {
afterEach(() => { afterEach(() => {
DatePickerComponent.mockClear(); vi.clearAllMocks();
}); });
test("It passes the loading prop to the DatePicker component", async () => { test("It passes the loading prop to the DatePicker component", async () => {
@@ -60,7 +61,7 @@ describe("Tests for DatePicker Component", () => {
test("when there are only slots in the next month, skip the current month", async () => { test("when there are only slots in the next month, skip the current month", async () => {
// there'll be one slot open on this day, next month. // there'll be one slot open on this day, next month.
const slotDate = dayjs().add("1", "month"); const slotDate = dayjs().add(1, "month");
render( render(
<DatePicker <DatePicker
event={{}} event={{}}
@@ -72,7 +73,12 @@ describe("Tests for DatePicker Component", () => {
], ],
}} }}
isLoading={false} isLoading={false}
/> />,
{
mockStore: {
month: slotDate.format("YYYY-MM"), // Start with next month to simulate the auto-advance
},
}
); );
// slot date is next month, so it'll have skipped the current month // slot date is next month, so it'll have skipped the current month
// by setting the browsingDate to the next month. // by setting the browsingDate to the next month.
@@ -94,13 +100,18 @@ describe("Tests for DatePicker Component", () => {
} }
} }
isLoading={false} isLoading={false}
/> />,
{
mockStore: {
month: dayjs().add(1, "month").format("YYYY-MM"), // Start with next month to simulate the auto-advance
},
}
); );
// slot date is next month, so it'll have skipped the current month // slot date is next month, so it'll have skipped the current month
// by setting the browsingDate to the next month. // by setting the browsingDate to the next month.
expect(DatePickerComponent).toHaveBeenCalledWith( expect(DatePickerComponent).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
browsingDate: dayjs().add("1", "month").startOf("month"), browsingDate: dayjs().add(1, "month").startOf("month"),
}), }),
expect.anything() expect.anything()
); );
@@ -2,6 +2,7 @@ import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs"; import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { DatePickerClassNames } from "@calcom/features/bookings/Booker/types"; import type { DatePickerClassNames } from "@calcom/features/bookings/Booker/types";
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker"; import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays"; import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
@@ -11,7 +12,6 @@ import type { User } from "@calcom/prisma/client";
import type { PeriodData } from "@calcom/types/Event"; import type { PeriodData } from "@calcom/types/Event";
import type { Slots } from "../../types"; import type { Slots } from "../../types";
import { useBookerStore } from "../store";
const useMoveToNextMonthOnNoAvailability = ({ const useMoveToNextMonthOnNoAvailability = ({
browsingDate, browsingDate,
@@ -76,12 +76,12 @@ export const DatePicker = ({
showNoAvailabilityDialog?: boolean; showNoAvailabilityDialog?: boolean;
}) => { }) => {
const { i18n } = useLocale(); const { i18n } = useLocale();
const [month, selectedDate, layout] = useBookerStore( const [month, selectedDate, layout] = useBookerStoreContext(
(state) => [state.month, state.selectedDate, state.layout], (state) => [state.month, state.selectedDate, state.layout],
shallow shallow
); );
const [setSelectedDate, setMonth, setDayCount] = useBookerStore( const [setSelectedDate, setMonth, setDayCount] = useBookerStoreContext(
(state) => [state.setSelectedDate, state.setMonth, state.setDayCount], (state) => [state.setSelectedDate, state.setMonth, state.setDayCount],
shallow shallow
); );
@@ -6,6 +6,7 @@ import { shallow } from "zustand/shallow";
import { Timezone as PlatformTimezoneSelect } from "@calcom/atoms/timezone"; import { Timezone as PlatformTimezoneSelect } from "@calcom/atoms/timezone";
import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings"; import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { Timezone } from "@calcom/features/bookings/Booker/types"; import type { Timezone } from "@calcom/features/bookings/Booker/types";
import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText"; import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText";
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details"; import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
@@ -19,7 +20,6 @@ import { EventTypeAutoTranslatedField } from "@calcom/prisma/enums";
import i18nConfigration from "../../../../../i18n.json"; import i18nConfigration from "../../../../../i18n.json";
import { fadeInUp } from "../config"; import { fadeInUp } from "../config";
import { useBookerStore } from "../store";
import { FromToTime } from "../utils/dates"; import { FromToTime } from "../utils/dates";
import { useBookerTime } from "./hooks/useBookerTime"; import { useBookerTime } from "./hooks/useBookerTime";
@@ -54,7 +54,8 @@ export const EventMeta = ({
locale, locale,
timeZones, timeZones,
children, children,
roundRobinHideOrgAndTeam, selectedTimeslot,
roundRobinHideOrgAndTeam
}: { }: {
event?: Pick< event?: Pick<
BookerEvent, BookerEvent,
@@ -91,17 +92,17 @@ export const EventMeta = ({
locale?: string | null; locale?: string | null;
timeZones?: Timezone[]; timeZones?: Timezone[];
children?: React.ReactNode; children?: React.ReactNode;
selectedTimeslot: string | null;
roundRobinHideOrgAndTeam?: boolean; roundRobinHideOrgAndTeam?: boolean;
}) => { }) => {
const { timeFormat, timezone } = useBookerTime(); const { timeFormat, timezone } = useBookerTime();
const [setTimezone] = useTimePreferences((state) => [state.setTimezone]); const [setTimezone] = useTimePreferences((state) => [state.setTimezone]);
const [setBookerStoreTimezone] = useBookerStore((state) => [state.setTimezone], shallow); const [setBookerStoreTimezone] = useBookerStoreContext((state) => [state.setTimezone], shallow);
const selectedDuration = useBookerStore((state) => state.selectedDuration); const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot); const bookerState = useBookerStoreContext((state) => state.state);
const bookerState = useBookerStore((state) => state.state); const bookingData = useBookerStoreContext((state) => state.bookingData);
const bookingData = useBookerStore((state) => state.bookingData); const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
const rescheduleUid = useBookerStore((state) => state.rescheduleUid); const [seatedEventData, setSeatedEventData] = useBookerStoreContext(
const [seatedEventData, setSeatedEventData] = useBookerStore(
(state) => [state.seatedEventData, state.setSeatedEventData], (state) => [state.seatedEventData, state.setSeatedEventData],
shallow shallow
); );
@@ -4,6 +4,7 @@ import { shallow } from "zustand/shallow";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { BookerLayouts } from "@calcom/prisma/zod-utils"; import { BookerLayouts } from "@calcom/prisma/zod-utils";
@@ -14,7 +15,6 @@ import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip"; import { Tooltip } from "@calcom/ui/components/tooltip";
import { TimeFormatToggle } from "../../components/TimeFormatToggle"; import { TimeFormatToggle } from "../../components/TimeFormatToggle";
import { useBookerStore } from "../store";
import type { BookerLayout } from "../types"; import type { BookerLayout } from "../types";
export function Header({ export function Header({
@@ -36,10 +36,10 @@ export function Header({
}) { }) {
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
const isEmbed = useIsEmbed(); const isEmbed = useIsEmbed();
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow); const [layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow);
const selectedDateString = useBookerStore((state) => state.selectedDate); const selectedDateString = useBookerStoreContext((state) => state.selectedDate);
const setSelectedDate = useBookerStore((state) => state.setSelectedDate); const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
const addToSelectedDate = useBookerStore((state) => state.addToSelectedDate); const addToSelectedDate = useBookerStoreContext((state) => state.addToSelectedDate);
const isMonthView = layout === BookerLayouts.MONTH_VIEW; const isMonthView = layout === BookerLayouts.MONTH_VIEW;
const today = dayjs(); const today = dayjs();
const selectedDate = selectedDateString ? dayjs(selectedDateString) : today; const selectedDate = selectedDateString ? dayjs(selectedDateString) : today;
@@ -1,13 +1,13 @@
import { useMemo, useEffect } from "react"; import { useMemo, useEffect } from "react";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { BookerEvent } from "@calcom/features/bookings/types"; import type { BookerEvent } from "@calcom/features/bookings/types";
import { Calendar } from "@calcom/features/calendars/weeklyview"; import { Calendar } from "@calcom/features/calendars/weeklyview";
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events"; import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state"; import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
import { localStorage } from "@calcom/lib/webstorage"; import { localStorage } from "@calcom/lib/webstorage";
import { useBookerStore } from "../store";
import type { useScheduleForEventReturnType } from "../utils/event"; import type { useScheduleForEventReturnType } from "../utils/event";
import { getQueryParam } from "../utils/query-param"; import { getQueryParam } from "../utils/query-param";
import { useOverlayCalendarStore } from "./OverlayCalendar/store"; import { useOverlayCalendarStore } from "./OverlayCalendar/store";
@@ -25,9 +25,9 @@ export const LargeCalendar = ({
data?: Pick<BookerEvent, "length"> | null; data?: Pick<BookerEvent, "length"> | null;
}; };
}) => { }) => {
const selectedDate = useBookerStore((state) => state.selectedDate); const selectedDate = useBookerStoreContext((state) => state.selectedDate);
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot); const setSelectedTimeslot = useBookerStoreContext((state) => state.setSelectedTimeslot);
const selectedEventDuration = useBookerStore((state) => state.selectedDuration); const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration);
const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates); const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates);
const displayOverlay = const displayOverlay =
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault"); getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");
@@ -1,11 +1,11 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Switch } from "@calcom/ui/components/form";
import { Button } from "@calcom/ui/components/button";
import classNames from "@calcom/ui/classNames"; import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { Switch } from "@calcom/ui/components/form";
import { useBookerStore } from "../../store";
import { useOverlayCalendarStore } from "./store"; import { useOverlayCalendarStore } from "./store";
interface OverlayCalendarSwitchProps { interface OverlayCalendarSwitchProps {
@@ -20,7 +20,7 @@ export function OverlayCalendarSwitch({ enabled, hasSession, onStateChange }: Ov
const setCalendarSettingsOverlay = useOverlayCalendarStore( const setCalendarSettingsOverlay = useOverlayCalendarStore(
(state) => state.setCalendarSettingsOverlayModal (state) => state.setCalendarSettingsOverlayModal
); );
const layout = useBookerStore((state) => state.layout); const layout = useBookerStoreContext((state) => state.layout);
const switchEnabled = enabled; const switchEnabled = enabled;
/** /**
@@ -2,9 +2,9 @@ import type { MotionProps } from "framer-motion";
import { m } from "framer-motion"; import { m } from "framer-motion";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import classNames from "@calcom/ui/classNames"; import classNames from "@calcom/ui/classNames";
import { useBookerStore } from "../store";
import type { BookerAreas, BookerLayout } from "../types"; import type { BookerAreas, BookerLayout } from "../types";
/** /**
@@ -44,7 +44,7 @@ export const BookerSection = forwardRef<HTMLDivElement, BookerSectionProps>(func
{ children, area, visible, className, ...props }, { children, area, visible, className, ...props },
ref ref
) { ) {
const layout = useBookerStore((state) => state.layout); const layout = useBookerStoreContext((state) => state.layout);
let gridClassName: string; let gridClassName: string;
if (typeof area === "string") { if (typeof area === "string") {
@@ -2,13 +2,13 @@ import { useEffect, useRef } from "react";
import { shallow } from "zustand/shallow"; import { shallow } from "zustand/shallow";
import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { BookerEvent } from "@calcom/features/bookings/types"; import type { BookerEvent } from "@calcom/features/bookings/types";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import type { BookerLayouts } from "@calcom/prisma/zod-utils"; import type { BookerLayouts } from "@calcom/prisma/zod-utils";
import { defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils"; import { defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
import { extraDaysConfig } from "../../config"; import { extraDaysConfig } from "../../config";
import { useBookerStore } from "../../store";
import type { BookerLayout } from "../../types"; import type { BookerLayout } from "../../types";
import { validateLayout } from "../../utils/layout"; import { validateLayout } from "../../utils/layout";
import { getQueryParam } from "../../utils/query-param"; import { getQueryParam } from "../../utils/query-param";
@@ -18,7 +18,7 @@ export type UseBookerLayoutType = ReturnType<typeof useBookerLayout>;
export const useBookerLayout = ( export const useBookerLayout = (
profileBookerLayouts: BookerEvent["profile"]["bookerLayouts"] | undefined | null profileBookerLayouts: BookerEvent["profile"]["bookerLayouts"] | undefined | null
) => { ) => {
const [_layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow); const [_layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow);
const isEmbed = useIsEmbed(); const isEmbed = useIsEmbed();
const isMobile = useMediaQuery("(max-width: 768px)"); const isMobile = useMediaQuery("(max-width: 768px)");
const isTablet = useMediaQuery("(max-width: 1024px)"); const isTablet = useMediaQuery("(max-width: 1024px)");
@@ -1,12 +1,12 @@
import { shallow } from "zustand/shallow"; import { shallow } from "zustand/shallow";
import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useTimePreferences } from "../../../lib/timePreferences"; import { useTimePreferences } from "../../../lib/timePreferences";
import { getBookerTimezone } from "../../utils/getBookerTimezone"; import { getBookerTimezone } from "../../utils/getBookerTimezone";
export const useBookerTime = () => { export const useBookerTime = () => {
const [timezoneFromBookerStore] = useBookerStore((state) => [state.timezone], shallow); const [timezoneFromBookerStore] = useBookerStoreContext((state) => [state.timezone], shallow);
const { timezone: timezoneFromTimePreferences, timeFormat } = useTimePreferences(); const { timezone: timezoneFromTimePreferences, timeFormat } = useTimePreferences();
const timezone = getBookerTimezone({ const timezone = getBookerTimezone({
storeTimezone: timezoneFromBookerStore, storeTimezone: timezoneFromBookerStore,
@@ -8,6 +8,7 @@ import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import { useHandleBookEvent } from "@calcom/atoms/hooks/bookings/useHandleBookEvent"; import { useHandleBookEvent } from "@calcom/atoms/hooks/bookings/useHandleBookEvent";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { sdkActionManager } from "@calcom/embed-core/embed-iframe"; import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { updateQueryParam, getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param"; import { updateQueryParam, getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
import { createBooking, createRecurringBooking, createInstantBooking } from "@calcom/features/bookings/lib"; import { createBooking, createRecurringBooking, createInstantBooking } from "@calcom/features/bookings/lib";
@@ -117,21 +118,21 @@ export const useBookings = ({
isBookingDryRun, isBookingDryRun,
}: IUseBookings) => { }: IUseBookings) => {
const router = useRouter(); const router = useRouter();
const eventSlug = useBookerStore((state) => state.eventSlug); const eventSlug = useBookerStoreContext((state) => state.eventSlug);
const eventTypeId = useBookerStore((state) => state.eventId); const eventTypeId = useBookerStoreContext((state) => state.eventId);
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting); const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
const rescheduleUid = useBookerStore((state) => state.rescheduleUid); const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
const rescheduledBy = useBookerStore((state) => state.rescheduledBy); const rescheduledBy = useBookerStoreContext((state) => state.rescheduledBy);
const bookingData = useBookerStore((state) => state.bookingData); const bookingData = useBookerStoreContext((state) => state.bookingData);
const timeslot = useBookerStore((state) => state.selectedTimeslot); const timeslot = useBookerStoreContext((state) => state.selectedTimeslot);
const { t } = useLocale(); const { t } = useLocale();
const bookingSuccessRedirect = useBookingSuccessRedirect(); const bookingSuccessRedirect = useBookingSuccessRedirect();
const bookerFormErrorRef = useRef<HTMLDivElement>(null); const bookerFormErrorRef = useRef<HTMLDivElement>(null);
const [instantMeetingTokenExpiryTime, setExpiryTime] = useState<Date | undefined>(); const [instantMeetingTokenExpiryTime, setExpiryTime] = useState<Date | undefined>();
const [instantVideoMeetingUrl, setInstantVideoMeetingUrl] = useState<string | undefined>(); const [instantVideoMeetingUrl, setInstantVideoMeetingUrl] = useState<string | undefined>();
const duration = useBookerStore((state) => state.selectedDuration); const duration = useBookerStoreContext((state) => state.selectedDuration);
const isRescheduling = !!rescheduleUid && !!bookingData; const isRescheduling = !!rescheduleUid && !!bookingData;
@@ -2,7 +2,7 @@ import { useEffect, useRef } from "react";
import { shallow } from "zustand/shallow"; import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useSlotReservationId } from "@calcom/features/bookings/Booker/useSlotReservationId"; import { useSlotReservationId } from "@calcom/features/bookings/Booker/useSlotReservationId";
import { isBookingDryRun } from "@calcom/features/bookings/Booker/utils/isBookingDryRun"; import { isBookingDryRun } from "@calcom/features/bookings/Booker/utils/isBookingDryRun";
import { import {
@@ -78,10 +78,10 @@ const useQuickAvailabilityChecks = ({
export type UseSlotsReturnType = ReturnType<typeof useSlots>; export type UseSlotsReturnType = ReturnType<typeof useSlots>;
export const useSlots = (event: { id: number; length: number } | null) => { export const useSlots = (event: { id: number; length: number } | null) => {
const selectedDuration = useBookerStore((state) => state.selectedDuration); const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
const searchParams = useCompatSearchParams(); const searchParams = useCompatSearchParams();
const [selectedTimeslot, setSelectedTimeslot, tentativeSelectedTimeslots, setTentativeSelectedTimeslots] = const [selectedTimeslot, setSelectedTimeslot, tentativeSelectedTimeslots, setTentativeSelectedTimeslots] =
useBookerStore( useBookerStoreContext(
(state) => [ (state) => [
state.selectedTimeslot, state.selectedTimeslot,
state.setSelectedTimeslot, state.setSelectedTimeslot,
+253 -251
View File
@@ -15,7 +15,7 @@ import { updateQueryParam, getQueryParam, removeQueryParam } from "./utils/query
* Arguments passed into store initializer, containing * Arguments passed into store initializer, containing
* the event data. * the event data.
*/ */
type StoreInitializeType = { export type StoreInitializeType = {
username: string; username: string;
eventSlug: string; eventSlug: string;
// Month can be undefined if it's not passed in as a prop. // Month can be undefined if it's not passed in as a prop.
@@ -174,279 +174,281 @@ export type BookerStore = {
}; };
/** /**
* The booker store contains the data of the component's * Creates a new booker store instance
* current state. This data can be reused within child components
* by importing this hook.
*
* See comments in interface above for more information on it's specific values.
*/ */
export const useBookerStore = createWithEqualityFn<BookerStore>((set, get) => ({ export const createBookerStore = () =>
state: "loading", createWithEqualityFn<BookerStore>((set, get) => ({
setState: (state: BookerState) => set({ state }), state: "loading",
layout: BookerLayouts.MONTH_VIEW, setState: (state: BookerState) => set({ state }),
setLayout: (layout: BookerLayout) => { layout: BookerLayouts.MONTH_VIEW,
// If we switch to a large layout and don't have a date selected yet, setLayout: (layout: BookerLayout) => {
// we selected it here, so week title is rendered properly. // If we switch to a large layout and don't have a date selected yet,
if (["week_view", "column_view"].includes(layout) && !get().selectedDate) { // we selected it here, so week title is rendered properly.
set({ selectedDate: dayjs().format("YYYY-MM-DD") }); if (["week_view", "column_view"].includes(layout) && !get().selectedDate) {
} set({ selectedDate: dayjs().format("YYYY-MM-DD") });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("layout", layout);
}
return set({ layout });
},
selectedDate: getQueryParam("date") || null,
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
// unset selected date
if (!selectedDate) {
removeQueryParam("date");
return;
}
const currentSelection = dayjs(get().selectedDate);
const newSelection = dayjs(selectedDate);
set({ selectedDate });
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("date", selectedDate ?? "");
}
// Setting month make sure small calendar in fullscreen layouts also updates.
// preventMonthSwitching is true in monthly view
if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
} }
}
},
selectedDatesAndTimes: null,
setSelectedDatesAndTimes: (selectedDatesAndTimes) => {
set({ selectedDatesAndTimes });
},
addToSelectedDate: (days: number) => {
const currentSelection = dayjs(get().selectedDate);
let newSelection = currentSelection.add(days, "day");
// If newSelection is before the current date, set it to today
if (newSelection.isBefore(dayjs(), "day")) {
newSelection = dayjs();
}
const newSelectionFormatted = newSelection.format("YYYY-MM-DD");
if (newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
if (!get().isPlatform || get().allowUpdatingUrlParams) { if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", newSelection.format("YYYY-MM")); updateQueryParam("layout", layout);
}
return set({ layout });
},
selectedDate: getQueryParam("date") || null,
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
// unset selected date
if (!selectedDate) {
removeQueryParam("date");
return;
} }
}
set({ selectedDate: newSelectionFormatted }); const currentSelection = dayjs(get().selectedDate);
if (!get().isPlatform || get().allowUpdatingUrlParams) { const newSelection = dayjs(selectedDate);
updateQueryParam("date", newSelectionFormatted); set({ selectedDate });
} if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
}, updateQueryParam("date", selectedDate ?? "");
username: null, }
eventSlug: null,
eventId: null,
rescheduledBy: null,
verifiedEmail: null,
setVerifiedEmail: (email: string | null) => {
set({ verifiedEmail: email });
},
month:
getQueryParam("month") ||
(getQueryParam("date") && dayjs(getQueryParam("date")).isValid()
? dayjs(getQueryParam("date")).format("YYYY-MM")
: null) ||
dayjs().format("YYYY-MM"),
setMonth: (month: string | null) => {
if (!month) {
removeQueryParam("month");
return;
}
set({ month, selectedTimeslot: null });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", month ?? "");
}
get().setSelectedDate({ date: null });
},
dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null,
setDayCount: (dayCount: number | null) => {
set({ dayCount });
},
isTeamEvent: false,
seatedEventData: {
seatsPerTimeSlot: undefined,
attendees: undefined,
bookingUid: undefined,
showAvailableSeatsCount: true,
},
setSeatedEventData: (seatedEventData: SeatedEventData) => {
set({ seatedEventData });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null");
}
},
// This is different from timeZone in timePreferencesStore, because timeZone in timePreferencesStore is the preferred timezone of the booker,
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
timezone: getQueryParam("cal.tz") ?? null,
setTimezone: (timezone: string | null) => {
set({ timezone });
},
initialize: ({
username,
eventSlug,
month,
eventId,
rescheduleUid = null,
rescheduledBy = null,
bookingUid = null,
bookingData = null,
layout,
isTeamEvent,
durationConfig,
org,
isInstantMeeting,
timezone = null,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform = false,
allowUpdatingUrlParams = true,
}: StoreInitializeType) => {
const selectedDateInStore = get().selectedDate;
if ( // Setting month make sure small calendar in fullscreen layouts also updates.
get().username === username && // preventMonthSwitching is true in monthly view
get().eventSlug === eventSlug && if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) {
get().month === month && set({ month: newSelection.format("YYYY-MM") });
get().eventId === eventId && if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
get().rescheduleUid === rescheduleUid && updateQueryParam("month", newSelection.format("YYYY-MM"));
get().bookingUid === bookingUid && }
get().bookingData?.responses.email === bookingData?.responses.email && }
get().layout === layout && },
get().timezone === timezone && selectedDatesAndTimes: null,
get().rescheduledBy === rescheduledBy && setSelectedDatesAndTimes: (selectedDatesAndTimes) => {
get().teamMemberEmail === teamMemberEmail && set({ selectedDatesAndTimes });
get().crmOwnerRecordType === crmOwnerRecordType && },
get().crmAppSlug === crmAppSlug && addToSelectedDate: (days: number) => {
get().crmRecordId === crmRecordId const currentSelection = dayjs(get().selectedDate);
) let newSelection = currentSelection.add(days, "day");
return;
set({ // If newSelection is before the current date, set it to today
if (newSelection.isBefore(dayjs(), "day")) {
newSelection = dayjs();
}
const newSelectionFormatted = newSelection.format("YYYY-MM-DD");
if (newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
}
}
set({ selectedDate: newSelectionFormatted });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("date", newSelectionFormatted);
}
},
username: null,
eventSlug: null,
eventId: null,
rescheduledBy: null,
verifiedEmail: null,
setVerifiedEmail: (email: string | null) => {
set({ verifiedEmail: email });
},
month:
getQueryParam("month") ||
(getQueryParam("date") && dayjs(getQueryParam("date")).isValid()
? dayjs(getQueryParam("date")).format("YYYY-MM")
: null) ||
dayjs().format("YYYY-MM"),
setMonth: (month: string | null) => {
if (!month) {
removeQueryParam("month");
return;
}
set({ month, selectedTimeslot: null });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", month ?? "");
}
get().setSelectedDate({ date: null });
},
dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null,
setDayCount: (dayCount: number | null) => {
set({ dayCount });
},
isTeamEvent: false,
seatedEventData: {
seatsPerTimeSlot: undefined,
attendees: undefined,
bookingUid: undefined,
showAvailableSeatsCount: true,
},
setSeatedEventData: (seatedEventData: SeatedEventData) => {
set({ seatedEventData });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null");
}
},
// This is different from timeZone in timePreferencesStore, because timeZone in timePreferencesStore is the preferred timezone of the booker,
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
timezone: getQueryParam("cal.tz") ?? null,
setTimezone: (timezone: string | null) => {
set({ timezone });
},
initialize: ({
username, username,
eventSlug, eventSlug,
month,
eventId, eventId,
org, rescheduleUid = null,
rescheduleUid, rescheduledBy = null,
rescheduledBy, bookingUid = null,
bookingUid, bookingData = null,
bookingData, layout,
layout: layout || BookerLayouts.MONTH_VIEW, isTeamEvent,
isTeamEvent: isTeamEvent || false,
durationConfig, durationConfig,
timezone, org,
// Preselect today's date in week / column view, since they use this to show the week title. isInstantMeeting,
selectedDate: timezone = null,
selectedDateInStore ||
(["week_view", "column_view"].includes(layout) ? dayjs().format("YYYY-MM-DD") : null),
teamMemberEmail, teamMemberEmail,
crmOwnerRecordType, crmOwnerRecordType,
crmAppSlug, crmAppSlug,
crmRecordId, crmRecordId,
isPlatform, isPlatform = false,
allowUpdatingUrlParams, allowUpdatingUrlParams = true,
}); }: StoreInitializeType) => {
const selectedDateInStore = get().selectedDate;
if (durationConfig?.includes(Number(getQueryParam("duration")))) { if (
get().username === username &&
get().eventSlug === eventSlug &&
get().month === month &&
get().eventId === eventId &&
get().rescheduleUid === rescheduleUid &&
get().bookingUid === bookingUid &&
get().bookingData?.responses.email === bookingData?.responses.email &&
get().layout === layout &&
get().timezone === timezone &&
get().rescheduledBy === rescheduledBy &&
get().teamMemberEmail === teamMemberEmail &&
get().crmOwnerRecordType === crmOwnerRecordType &&
get().crmAppSlug === crmAppSlug &&
get().crmRecordId === crmRecordId
)
return;
set({ set({
selectedDuration: Number(getQueryParam("duration")), username,
eventSlug,
eventId,
org,
rescheduleUid,
rescheduledBy,
bookingUid,
bookingData,
layout: layout || BookerLayouts.MONTH_VIEW,
isTeamEvent: isTeamEvent || false,
durationConfig,
timezone,
// Preselect today's date in week / column view, since they use this to show the week title.
selectedDate:
selectedDateInStore ||
(["week_view", "column_view"].includes(layout) ? dayjs().format("YYYY-MM-DD") : null),
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform,
allowUpdatingUrlParams,
}); });
} else {
removeQueryParam("duration");
}
// Unset selected timeslot if user is rescheduling. This could happen if (durationConfig?.includes(Number(getQueryParam("duration")))) {
// if the user reschedules a booking right after the confirmation page. set({
// In that case the time would still be store in the store, this way we selectedDuration: Number(getQueryParam("duration")),
// force clear this. });
// Also, fetch the original booking duration if user is rescheduling and } else {
// update the selectedDuration removeQueryParam("duration");
if (rescheduleUid && bookingData) {
set({ selectedTimeslot: null });
const originalBookingLength = dayjs(bookingData?.endTime).diff(
dayjs(bookingData?.startTime),
"minutes"
);
set({ selectedDuration: originalBookingLength });
if (!isPlatform || allowUpdatingUrlParams) {
updateQueryParam("duration", originalBookingLength ?? "");
} }
}
if (month) set({ month });
if (isInstantMeeting) { // Unset selected timeslot if user is rescheduling. This could happen
const month = dayjs().format("YYYY-MM"); // if the user reschedules a booking right after the confirmation page.
const selectedDate = dayjs().format("YYYY-MM-DD"); // In that case the time would still be store in the store, this way we
const selectedTimeslot = new Date().toISOString(); // force clear this.
set({ // Also, fetch the original booking duration if user is rescheduling and
month, // update the selectedDuration
selectedDate, if (rescheduleUid && bookingData) {
selectedTimeslot, set({ selectedTimeslot: null });
isInstantMeeting, const originalBookingLength = dayjs(bookingData?.endTime).diff(
}); dayjs(bookingData?.startTime),
"minutes"
);
set({ selectedDuration: originalBookingLength });
if (!isPlatform || allowUpdatingUrlParams) {
updateQueryParam("duration", originalBookingLength ?? "");
}
}
if (month) set({ month });
if (!isPlatform || allowUpdatingUrlParams) { if (isInstantMeeting) {
updateQueryParam("month", month); const month = dayjs().format("YYYY-MM");
updateQueryParam("date", selectedDate ?? ""); const selectedDate = dayjs().format("YYYY-MM-DD");
const selectedTimeslot = new Date().toISOString();
set({
month,
selectedDate,
selectedTimeslot,
isInstantMeeting,
});
if (!isPlatform || allowUpdatingUrlParams) {
updateQueryParam("month", month);
updateQueryParam("date", selectedDate ?? "");
updateQueryParam("slot", selectedTimeslot ?? "", false);
}
}
//removeQueryParam("layout");
},
durationConfig: null,
selectedDuration: null,
setSelectedDuration: (selectedDuration: number | null) => {
set({ selectedDuration });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("duration", selectedDuration ?? "");
}
},
setBookingData: (bookingData: GetBookingType | null | undefined) => {
set({ bookingData: bookingData ?? null });
},
recurringEventCount: null,
setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }),
occurenceCount: null,
setOccurenceCount: (occurenceCount: number | null) => set({ occurenceCount }),
rescheduleUid: null,
bookingData: null,
bookingUid: null,
selectedTimeslot: getQueryParam("slot") || null,
tentativeSelectedTimeslots: [],
setTentativeSelectedTimeslots: (tentativeSelectedTimeslots: string[]) => {
set({ tentativeSelectedTimeslots });
},
setSelectedTimeslot: (selectedTimeslot: string | null) => {
set({ selectedTimeslot });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("slot", selectedTimeslot ?? "", false); updateQueryParam("slot", selectedTimeslot ?? "", false);
} }
} },
//removeQueryParam("layout"); formValues: {},
}, setFormValues: (formValues: Record<string, any>) => {
durationConfig: null, set({ formValues });
selectedDuration: null, },
setSelectedDuration: (selectedDuration: number | null) => { org: null,
set({ selectedDuration }); setOrg: (org: string | null | undefined) => {
if (!get().isPlatform || get().allowUpdatingUrlParams) { set({ org });
updateQueryParam("duration", selectedDuration ?? ""); },
} isPlatform: false,
}, allowUpdatingUrlParams: true,
setBookingData: (bookingData: GetBookingType | null | undefined) => { }));
set({ bookingData: bookingData ?? null });
}, /**
recurringEventCount: null, * Default global store instance for backward compatibility
setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }), */
occurenceCount: null, export const useBookerStore = createBookerStore();
setOccurenceCount: (occurenceCount: number | null) => set({ occurenceCount }),
rescheduleUid: null,
bookingData: null,
bookingUid: null,
selectedTimeslot: getQueryParam("slot") || null,
tentativeSelectedTimeslots: [],
setTentativeSelectedTimeslots: (tentativeSelectedTimeslots: string[]) => {
set({ tentativeSelectedTimeslots });
},
setSelectedTimeslot: (selectedTimeslot: string | null) => {
set({ selectedTimeslot });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("slot", selectedTimeslot ?? "", false);
}
},
formValues: {},
setFormValues: (formValues: Record<string, any>) => {
set({ formValues });
},
org: null,
setOrg: (org: string | null | undefined) => {
set({ org });
},
isPlatform: false,
allowUpdatingUrlParams: true,
}));
export const useInitializeBookerStore = ({ export const useInitializeBookerStore = ({
username, username,
@@ -1,11 +1,11 @@
import { shallow } from "zustand/shallow"; import { shallow } from "zustand/shallow";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useSchedule } from "@calcom/features/schedules/lib/use-schedule/useSchedule"; import { useSchedule } from "@calcom/features/schedules/lib/use-schedule/useSchedule";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { useBookerTime } from "../components/hooks/useBookerTime"; import { useBookerTime } from "../components/hooks/useBookerTime";
import { useBookerStore } from "../store";
export type useEventReturnType = ReturnType<typeof useEvent>; export type useEventReturnType = ReturnType<typeof useEvent>;
export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEvent>; export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEvent>;
@@ -19,7 +19,7 @@ export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEven
* of combining multiple conditional hooks. * of combining multiple conditional hooks.
*/ */
export const useEvent = (props?: { fromRedirectOfNonOrgLink?: boolean; disabled?: boolean }) => { export const useEvent = (props?: { fromRedirectOfNonOrgLink?: boolean; disabled?: boolean }) => {
const [username, eventSlug, isTeamEvent, org] = useBookerStore( const [username, eventSlug, isTeamEvent, org] = useBookerStoreContext(
(state) => [state.username, state.eventSlug, state.isTeamEvent, state.org], (state) => [state.username, state.eventSlug, state.isTeamEvent, state.org],
shallow shallow
); );
@@ -89,7 +89,7 @@ export const useScheduleForEvent = ({
useApiV2?: boolean; useApiV2?: boolean;
} = {}) => { } = {}) => {
const { timezone } = useBookerTime(); const { timezone } = useBookerTime();
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStore( const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStoreContext(
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration], (state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
shallow shallow
); );
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { OutOfOfficeInSlots } from "@calcom/features/bookings/Booker/components/OutOfOfficeInSlots"; import { OutOfOfficeInSlots } from "@calcom/features/bookings/Booker/components/OutOfOfficeInSlots";
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings"; import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
import type { BookerEvent } from "@calcom/features/bookings/types"; import type { BookerEvent } from "@calcom/features/bookings/types";
@@ -19,7 +20,6 @@ import { Icon } from "@calcom/ui/components/icon";
import { SkeletonText } from "@calcom/ui/components/skeleton"; import { SkeletonText } from "@calcom/ui/components/skeleton";
import { useBookerTime } from "../Booker/components/hooks/useBookerTime"; import { useBookerTime } from "../Booker/components/hooks/useBookerTime";
import { useBookerStore } from "../Booker/store";
import { getQueryParam } from "../Booker/utils/query-param"; import { getQueryParam } from "../Booker/utils/query-param";
import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay"; import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay";
import type { Slots } from "../types"; import type { Slots } from "../types";
@@ -112,8 +112,8 @@ const SlotItem = ({
getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault"); getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault");
const { timeFormat, timezone } = useBookerTime(); const { timeFormat, timezone } = useBookerTime();
const bookingData = useBookerStore((state) => state.bookingData); const bookingData = useBookerStoreContext((state) => state.bookingData);
const layout = useBookerStore((state) => state.layout); const layout = useBookerStoreContext((state) => state.layout);
const hasTimeSlots = !!seatsPerTimeSlot; const hasTimeSlots = !!seatsPerTimeSlot;
const computedDateWithUsersTimezone = dayjs.utc(slot.time).tz(timezone); const computedDateWithUsersTimezone = dayjs.utc(slot.time).tz(timezone);
@@ -127,7 +127,7 @@ const SlotItem = ({
const offset = (usersTimezoneDate.utcOffset() - nowDate.utcOffset()) / 60; const offset = (usersTimezoneDate.utcOffset() - nowDate.utcOffset()) / 60;
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot); const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
const { isOverlapping, overlappingTimeEnd, overlappingTimeStart } = useCheckOverlapWithOverlay({ const { isOverlapping, overlappingTimeEnd, overlappingTimeStart } = useCheckOverlapWithOverlay({
start: computedDateWithUsersTimezone, start: computedDateWithUsersTimezone,
@@ -2,12 +2,12 @@ import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs"; import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { nameOfDay } from "@calcom/lib/weekday"; import { nameOfDay } from "@calcom/lib/weekday";
import { BookerLayouts } from "@calcom/prisma/zod-utils"; import { BookerLayouts } from "@calcom/prisma/zod-utils";
import classNames from "@calcom/ui/classNames"; import classNames from "@calcom/ui/classNames";
import { useBookerStore } from "../Booker/store";
import { TimeFormatToggle } from "./TimeFormatToggle"; import { TimeFormatToggle } from "./TimeFormatToggle";
type AvailableTimesHeaderProps = { type AvailableTimesHeaderProps = {
@@ -28,7 +28,7 @@ export const AvailableTimesHeader = ({
customClassNames, customClassNames,
}: AvailableTimesHeaderProps) => { }: AvailableTimesHeaderProps) => {
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
const [layout] = useBookerStore((state) => [state.layout], shallow); const [layout] = useBookerStoreContext((state) => [state.layout], shallow);
const isColumnView = layout === BookerLayouts.COLUMN_VIEW; const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
const isMonthView = layout === BookerLayouts.MONTH_VIEW; const isMonthView = layout === BookerLayouts.MONTH_VIEW;
const isToday = dayjs().isSame(date, "day"); const isToday = dayjs().isSame(date, "day");
+3 -3
View File
@@ -4,7 +4,7 @@ import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs"; import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { useEmbedStyles } from "@calcom/embed-core/embed-iframe"; import { useEmbedStyles } from "@calcom/embed-core/embed-iframe";
import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth"; import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth";
import { daysInMonth, yyyymmdd } from "@calcom/lib/dayjs"; import { daysInMonth, yyyymmdd } from "@calcom/lib/dayjs";
import type { IFromUser, IToUser } from "@calcom/lib/getUserAvailability"; import type { IFromUser, IToUser } from "@calcom/lib/getUserAvailability";
@@ -227,7 +227,7 @@ const Days = ({
} }
} }
const [selectedDatesAndTimes] = useBookerStore((state) => [state.selectedDatesAndTimes], shallow); const [selectedDatesAndTimes] = useBookerStoreContext((state) => [state.selectedDatesAndTimes], shallow);
const isActive = (day: dayjs.Dayjs) => { const isActive = (day: dayjs.Dayjs) => {
// for selecting a range of dates // for selecting a range of dates
@@ -400,7 +400,7 @@ const DatePicker = ({
minDate && rawBrowsingDate.valueOf() < minDate.valueOf() ? dayjs(minDate) : rawBrowsingDate; minDate && rawBrowsingDate.valueOf() < minDate.valueOf() ? dayjs(minDate) : rawBrowsingDate;
const { i18n, t } = useLocale(); const { i18n, t } = useLocale();
const bookingData = useBookerStore((state) => state.bookingData); const bookingData = useBookerStoreContext((state) => state.bookingData);
const isBookingInPast = bookingData ? new Date(bookingData.endTime) < new Date() : false; const isBookingInPast = bookingData ? new Date(bookingData.endTime) < new Date() : false;
const changeMonth = (newMonth: number) => { const changeMonth = (newMonth: number) => {
if (onMonthChange) { if (onMonthChange) {
@@ -4,6 +4,7 @@ import React from "react";
import { vi } from "vitest"; import { vi } from "vitest";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { PeriodType } from "@calcom/prisma/enums"; import { PeriodType } from "@calcom/prisma/enums";
import { DatePicker } from "../DatePicker"; import { DatePicker } from "../DatePicker";
@@ -16,20 +17,22 @@ describe("Tests for DatePicker Component", () => {
test("Should render correctly with default date", async () => { test("Should render correctly with default date", async () => {
const testDate = dayjs("2024-02-20"); const testDate = dayjs("2024-02-20");
const { getByTestId } = render( const { getByTestId } = render(
<TooltipProvider> <BookerStoreProvider>
<DatePicker <TooltipProvider>
onChange={noop} <DatePicker
browsingDate={testDate} onChange={noop}
locale="en" browsingDate={testDate}
periodData={{ locale="en"
periodType: PeriodType.UNLIMITED, periodData={{
periodDays: null, periodType: PeriodType.UNLIMITED,
periodCountCalendarDays: false, periodDays: null,
periodStartDate: null, periodCountCalendarDays: false,
periodEndDate: null, periodStartDate: null,
}} periodEndDate: null,
/> }}
</TooltipProvider> />
</TooltipProvider>
</BookerStoreProvider>
); );
const selectedMonthLabel = getByTestId("selected-month-label"); const selectedMonthLabel = getByTestId("selected-month-label");
@@ -40,9 +43,11 @@ describe("Tests for DatePicker Component", () => {
const testDate = dayjs("2024-02-20"); const testDate = dayjs("2024-02-20");
const minDate = dayjs("2025-02-10"); const minDate = dayjs("2025-02-10");
const { getByTestId } = render( const { getByTestId } = render(
<TooltipProvider> <BookerStoreProvider>
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" /> <TooltipProvider>
</TooltipProvider> <DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
</TooltipProvider>
</BookerStoreProvider>
); );
const selectedMonthLabel = getByTestId("selected-month-label"); const selectedMonthLabel = getByTestId("selected-month-label");
@@ -53,9 +58,11 @@ describe("Tests for DatePicker Component", () => {
const testDate = dayjs("2025-03-20"); const testDate = dayjs("2025-03-20");
const minDate = dayjs("2025-02-10"); const minDate = dayjs("2025-02-10");
const { getByTestId } = render( const { getByTestId } = render(
<TooltipProvider> <BookerStoreProvider>
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" /> <TooltipProvider>
</TooltipProvider> <DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
</TooltipProvider>
</BookerStoreProvider>
); );
const selectedMonthLabel = getByTestId("selected-month-label"); const selectedMonthLabel = getByTestId("selected-month-label");
@@ -85,22 +92,24 @@ describe("Tests for DatePicker Component", () => {
]); ]);
const { getAllByTestId } = render( const { getAllByTestId } = render(
<TooltipProvider> <BookerStoreProvider>
<DatePicker <TooltipProvider>
onChange={noop} <DatePicker
browsingDate={earlyMonthDate} onChange={noop}
locale="en" browsingDate={earlyMonthDate}
slots={slots} locale="en"
isCompact={false} slots={slots}
periodData={{ isCompact={false}
periodType: PeriodType.UNLIMITED, periodData={{
periodDays: null, periodType: PeriodType.UNLIMITED,
periodCountCalendarDays: false, periodDays: null,
periodStartDate: null, periodCountCalendarDays: false,
periodEndDate: null, periodStartDate: null,
}} periodEndDate: null,
/> }}
</TooltipProvider> />
</TooltipProvider>
</BookerStoreProvider>
); );
const dayElements = getAllByTestId("day"); const dayElements = getAllByTestId("day");
@@ -127,22 +136,24 @@ describe("Tests for DatePicker Component", () => {
]); ]);
const { getAllByTestId, queryByText } = render( const { getAllByTestId, queryByText } = render(
<TooltipProvider> <BookerStoreProvider>
<DatePicker <TooltipProvider>
onChange={noop} <DatePicker
browsingDate={lateMonthDate} onChange={noop}
locale="en" browsingDate={lateMonthDate}
slots={slots} locale="en"
isCompact={false} slots={slots}
periodData={{ isCompact={false}
periodType: PeriodType.UNLIMITED, periodData={{
periodDays: null, periodType: PeriodType.UNLIMITED,
periodCountCalendarDays: false, periodDays: null,
periodStartDate: null, periodCountCalendarDays: false,
periodEndDate: null, periodStartDate: null,
}} periodEndDate: null,
/> }}
</TooltipProvider> />
</TooltipProvider>
</BookerStoreProvider>
); );
const dayElements = getAllByTestId("day"); const dayElements = getAllByTestId("day");
@@ -165,22 +176,24 @@ describe("Tests for DatePicker Component", () => {
const slots = createMockSlots(["2024-01-25", "2024-02-01"]); const slots = createMockSlots(["2024-01-25", "2024-02-01"]);
const { getAllByTestId } = render( const { getAllByTestId } = render(
<TooltipProvider> <BookerStoreProvider>
<DatePicker <TooltipProvider>
onChange={noop} <DatePicker
browsingDate={lateMonthDate} onChange={noop}
locale="en" browsingDate={lateMonthDate}
slots={slots} locale="en"
isCompact={true} // This should force traditional view slots={slots}
periodData={{ isCompact={true} // This should force traditional view
periodType: PeriodType.UNLIMITED, periodData={{
periodDays: null, periodType: PeriodType.UNLIMITED,
periodCountCalendarDays: false, periodDays: null,
periodStartDate: null, periodCountCalendarDays: false,
periodEndDate: null, periodStartDate: null,
}} periodEndDate: null,
/> }}
</TooltipProvider> />
</TooltipProvider>
</BookerStoreProvider>
); );
const dayElements = getAllByTestId("day"); const dayElements = getAllByTestId("day");
+45 -30
View File
@@ -11,7 +11,12 @@ import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs"; import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { AvailableTimes, AvailableTimesHeader } from "@calcom/features/bookings"; import { AvailableTimes, AvailableTimesHeader } from "@calcom/features/bookings";
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"; import {
BookerStoreProvider,
useInitializeBookerStoreContext,
useBookerStoreContext,
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event"; import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
import DatePicker from "@calcom/features/calendars/DatePicker"; import DatePicker from "@calcom/features/calendars/DatePicker";
import { Dialog } from "@calcom/features/components/controlled-dialog"; import { Dialog } from "@calcom/features/components/controlled-dialog";
@@ -260,13 +265,21 @@ const EmailEmbed = ({
org: orgSlug, org: orgSlug,
isTeamEvent, isTeamEvent,
}); });
useInitializeBookerStoreContext({
username,
eventSlug: eventType?.slug ?? "",
eventId: eventType?.id,
layout: BookerLayouts.MONTH_VIEW,
org: orgSlug,
isTeamEvent,
});
const [month, selectedDate, selectedDatesAndTimes] = useBookerStore( const [month, selectedDate, selectedDatesAndTimes] = useBookerStoreContext(
(state) => [state.month, state.selectedDate, state.selectedDatesAndTimes], (state) => [state.month, state.selectedDate, state.selectedDatesAndTimes],
shallow shallow
); );
const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot, setTimezone] = const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot, setTimezone] =
useBookerStore( useBookerStoreContext(
(state) => [ (state) => [
state.setSelectedDate, state.setSelectedDate,
state.setMonth, state.setMonth,
@@ -686,7 +699,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
const emailContentRef = useRef<HTMLDivElement>(null); const emailContentRef = useRef<HTMLDivElement>(null);
const { data } = useSession(); const { data } = useSession();
const [month, selectedDatesAndTimes] = useBookerStore( const [month, selectedDatesAndTimes] = useBookerStoreContext(
(state) => [state.month, state.selectedDatesAndTimes], (state) => [state.month, state.selectedDatesAndTimes],
shallow shallow
); );
@@ -1369,32 +1382,34 @@ export const EmbedDialog = ({
}; };
return ( return (
<Dialog <BookerStoreProvider>
{...(noQueryParamMode <Dialog
? { {...(noQueryParamMode
open: embedState !== null, ? {
onOpenChange: (open) => !open && handleDialogClose(), open: embedState !== null,
} onOpenChange: (open) => !open && handleDialogClose(),
: { }
// Must not set name when noQueryParam mode as required by Dialog component : {
name: "embed", // Must not set name when noQueryParam mode as required by Dialog component
clearQueryParamsOnClose: queryParamsForDialog, name: "embed",
})}> clearQueryParamsOnClose: queryParamsForDialog,
{!embedParams.embedType ? ( })}>
<ChooseEmbedTypesDialogContent types={types} noQueryParamMode={noQueryParamMode} /> {!embedParams.embedType ? (
) : ( <ChooseEmbedTypesDialogContent types={types} noQueryParamMode={noQueryParamMode} />
<EmbedTypeCodeAndPreviewDialogContent ) : (
embedType={embedParams.embedType as EmbedType} <EmbedTypeCodeAndPreviewDialogContent
embedUrl={embedParams.embedUrl} embedType={embedParams.embedType as EmbedType}
namespace={embedParams.namespace} embedUrl={embedParams.embedUrl}
tabs={tabs} namespace={embedParams.namespace}
types={types} tabs={tabs}
eventTypeHideOptionDisabled={eventTypeHideOptionDisabled} types={types}
defaultBrandColor={defaultBrandColor} eventTypeHideOptionDisabled={eventTypeHideOptionDisabled}
noQueryParamMode={noQueryParamMode} defaultBrandColor={defaultBrandColor}
/> noQueryParamMode={noQueryParamMode}
)} />
</Dialog> )}
</Dialog>
</BookerStoreProvider>
); );
}; };
@@ -1,3 +1,4 @@
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import classNames from "@calcom/ui/classNames"; import classNames from "@calcom/ui/classNames";
@@ -63,5 +64,9 @@ const TroubleshooterComponent = ({ month }: TroubleshooterProps) => {
}; };
export const Troubleshooter = ({ month }: TroubleshooterProps) => { export const Troubleshooter = ({ month }: TroubleshooterProps) => {
return <TroubleshooterComponent month={month} />; return (
<BookerStoreProvider>
<TroubleshooterComponent month={month} />
</BookerStoreProvider>
);
}; };
+1
View File
@@ -72,6 +72,7 @@ export async function getConnectedApps({
if (includeTeamInstalledApps || teamId) { if (includeTeamInstalledApps || teamId) {
const teamsQuery = await prisma.team.findMany({ const teamsQuery = await prisma.team.findMany({
where: { where: {
...(teamId ? { id: teamId } : {}),
members: { members: {
some: { some: {
userId: user.id, userId: user.id,
@@ -229,4 +229,61 @@ export class CredentialRepository {
static async updateWhereId({ id, data }: { id: number; data: { key: Prisma.InputJsonValue } }) { static async updateWhereId({ id, data }: { id: number; data: { key: Prisma.InputJsonValue } }) {
return prisma.credential.update({ where: { id }, data }); 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,
},
});
}
} }
+17
View File
@@ -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;
}
} }
+6
View File
@@ -1,5 +1,11 @@
## 1.1.2 ## 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 ## 1.3.0
### Minor Changes ### Minor Changes
@@ -13,6 +13,7 @@ import React, {
import { Controller, useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form"; import { Controller, useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { Dialog } from "@calcom/features/components/controlled-dialog"; import { Dialog } from "@calcom/features/components/controlled-dialog";
import { TimezoneSelect as WebTimezoneSelect } from "@calcom/features/components/timezone-select"; import { TimezoneSelect as WebTimezoneSelect } from "@calcom/features/components/timezone-select";
import type { import type {
@@ -672,19 +673,21 @@ export const AvailabilitySettings = forwardRef<AvailabilitySettingsFormRef, Avai
</div> </div>
</div> </div>
{enableOverrides && ( {enableOverrides && (
<DateOverride <BookerStoreProvider>
workingHours={schedule.workingHours} <DateOverride
userTimeFormat={timeFormat} workingHours={schedule.workingHours}
handleSubmit={handleSubmit} userTimeFormat={timeFormat}
travelSchedules={travelSchedules} handleSubmit={handleSubmit}
weekStart={ travelSchedules={travelSchedules}
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf( weekStart={
weekStart ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
) as 0 | 1 | 2 | 3 | 4 | 5 | 6 weekStart
} ) as 0 | 1 | 2 | 3 | 4 | 5 | 6
overridesModalClassNames={customClassNames?.overridesModalClassNames} }
classNames={customClassNames?.dateOverrideClassNames} overridesModalClassNames={customClassNames?.overridesModalClassNames}
/> classNames={customClassNames?.dateOverrideClassNames}
/>
</BookerStoreProvider>
)} )}
</div> </div>
<div className="min-w-40 col-span-3 hidden space-y-2 md:block lg:col-span-1"> <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 dayjs from "@calcom/dayjs";
import { Booker as BookerComponent } 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 { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm"; import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
import { useLocalSet } from "@calcom/features/bookings/Booker/components/hooks/useLocalSet"; import { useLocalSet } from "@calcom/features/bookings/Booker/components/hooks/useLocalSet";
@@ -40,7 +45,7 @@ import type {
BookerStoreValues, BookerStoreValues,
} from "./types"; } from "./types";
export const BookerPlatformWrapper = ( const BookerPlatformWrapperComponent = (
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
) => { ) => {
const { const {
@@ -64,15 +69,18 @@ export const BookerPlatformWrapper = (
const { clientId } = useAtomsContext(); const { clientId } = useAtomsContext();
const teamId: number | undefined = props.isTeamEvent ? props.teamId : undefined; const teamId: number | undefined = props.isTeamEvent ? props.teamId : undefined;
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow); const [bookerState, setBookerState] = useBookerStoreContext(
const setSelectedDate = useBookerStore((state) => state.setSelectedDate); (state) => [state.state, state.setState],
const setSelectedDuration = useBookerStore((state) => state.setSelectedDuration); shallow
const setBookingData = useBookerStore((state) => state.setBookingData); );
const setOrg = useBookerStore((state) => state.setOrg); const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
const bookingData = useBookerStore((state) => state.bookingData); const setSelectedDuration = useBookerStoreContext((state) => state.setSelectedDuration);
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot); const setBookingData = useBookerStoreContext((state) => state.setBookingData);
const setSelectedMonth = useBookerStore((state) => state.setMonth); const setOrg = useBookerStoreContext((state) => state.setOrg);
const selectedDuration = useBookerStore((state) => state.selectedDuration); 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( const [isOverlayCalendarEnabled, setIsOverlayCalendarEnabled] = useState(
Boolean(localStorage?.getItem?.("overlayCalendarSwitchDefault")) Boolean(localStorage?.getItem?.("overlayCalendarSwitchDefault"))
@@ -171,11 +179,27 @@ export const BookerPlatformWrapper = (
isPlatform: true, isPlatform: true,
allowUpdatingUrlParams, allowUpdatingUrlParams,
}); });
const [dayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow); useInitializeBookerStoreContext({
const selectedDate = useBookerStore((state) => state.selectedDate); ...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 month = useBookerStoreContext((state) => state.month);
const eventSlug = useBookerStore((state) => state.eventSlug); const eventSlug = useBookerStoreContext((state) => state.eventSlug);
const { data: session } = useMe(); const { data: session } = useMe();
const hasSession = !!session; const hasSession = !!session;
@@ -557,6 +581,16 @@ export const BookerPlatformWrapper = (
); );
}; };
export const BookerPlatformWrapper = (
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
) => {
return (
<BookerStoreProvider>
<BookerPlatformWrapperComponent {...props} />
</BookerStoreProvider>
);
};
function formatUsername(username: string | string[]): string { function formatUsername(username: string | string[]): string {
if (typeof username === "string") { if (typeof username === "string") {
return username; return username;
@@ -4,6 +4,7 @@ import { useSession } from "next-auth/react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useMemo, useCallback, useEffect } from "react"; import { useMemo, useCallback, useEffect } from "react";
import React from "react";
import { shallow } from "zustand/shallow"; import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs"; 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 { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import type { BookerProps } from "@calcom/features/bookings/Booker"; import type { BookerProps } from "@calcom/features/bookings/Booker";
import { Booker as BookerComponent } 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 { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm"; import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
import { useBookings } from "@calcom/features/bookings/Booker/components/hooks/useBookings"; 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 { useSlots } from "@calcom/features/bookings/Booker/components/hooks/useSlots";
import { useVerifyCode } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode"; import { useVerifyCode } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode";
import { useVerifyEmail } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail"; 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 { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
import { useBrandColors } from "@calcom/features/bookings/Booker/utils/use-brand-colors"; import { useBrandColors } from "@calcom/features/bookings/Booker/utils/use-brand-colors";
import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent"; 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 { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { localStorage } from "@calcom/lib/webstorage";
import { BookerLayouts } from "@calcom/prisma/zod-utils"; import { BookerLayouts } from "@calcom/prisma/zod-utils";
export type BookerWebWrapperAtomProps = BookerProps & { export type BookerWebWrapperAtomProps = BookerProps & {
eventData?: NonNullable<Awaited<ReturnType<typeof getPublicEvent>>>; eventData?: NonNullable<Awaited<ReturnType<typeof getPublicEvent>>>;
}; };
export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => { const BookerPlatformWrapperComponent = (props: BookerWebWrapperAtomProps) => {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@@ -74,10 +81,20 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
org: props.entity.orgSlug, org: props.entity.orgSlug,
timezone, 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 [bookerState, _] = useBookerStoreContext((state) => [state.state, state.setState], shallow);
const [dayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow); const [dayCount] = useBookerStoreContext((state) => [state.dayCount, state.setDayCount], shallow);
const [month] = useBookerStore((state) => [state.month, state.setMonth], shallow); const [month] = useBookerStoreContext((state) => [state.month, state.setMonth], shallow);
const { data: session } = useSession(); const { data: session } = useSession();
const routerQuery = useRouterQuery(); const routerQuery = useRouterQuery();
@@ -263,3 +280,11 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
/> />
); );
}; };
export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
return (
<BookerStoreProvider>
<BookerPlatformWrapperComponent {...props} />
</BookerStoreProvider>
);
};
@@ -1,9 +1,9 @@
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; 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 { useBookerTime } from "@calcom/features/bookings/Booker/components/hooks/useBookerTime";
import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm"; 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 { mapBookingToMutationInput, mapRecurringBookingToMutationInput } from "@calcom/features/bookings/lib";
import type { BookerEvent } from "@calcom/features/bookings/types"; import type { BookerEvent } from "@calcom/features/bookings/types";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -46,23 +46,23 @@ export const useHandleBookEvent = ({
isBookingDryRun, isBookingDryRun,
}: UseHandleBookingProps) => { }: UseHandleBookingProps) => {
const isPlatform = useIsPlatform(); const isPlatform = useIsPlatform();
const setFormValues = useBookerStore((state) => state.setFormValues); const setFormValues = useBookerStoreContext((state) => state.setFormValues);
const storeTimeSlot = useBookerStore((state) => state.selectedTimeslot); const storeTimeSlot = useBookerStoreContext((state) => state.selectedTimeslot);
const duration = useBookerStore((state) => state.selectedDuration); const duration = useBookerStoreContext((state) => state.selectedDuration);
const { timezone } = useBookerTime(); const { timezone } = useBookerTime();
const rescheduleUid = useBookerStore((state) => state.rescheduleUid); const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
const rescheduledBy = useBookerStore((state) => state.rescheduledBy); const rescheduledBy = useBookerStoreContext((state) => state.rescheduledBy);
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
const username = useBookerStore((state) => state.username); const username = useBookerStoreContext((state) => state.username);
const recurringEventCount = useBookerStore((state) => state.recurringEventCount); const recurringEventCount = useBookerStoreContext((state) => state.recurringEventCount);
const bookingData = useBookerStore((state) => state.bookingData); const bookingData = useBookerStoreContext((state) => state.bookingData);
const seatedEventData = useBookerStore((state) => state.seatedEventData); const seatedEventData = useBookerStoreContext((state) => state.seatedEventData);
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting); const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
const orgSlug = useBookerStore((state) => state.org); const orgSlug = useBookerStoreContext((state) => state.org);
const teamMemberEmail = useBookerStore((state) => state.teamMemberEmail); const teamMemberEmail = useBookerStoreContext((state) => state.teamMemberEmail);
const crmOwnerRecordType = useBookerStore((state) => state.crmOwnerRecordType); const crmOwnerRecordType = useBookerStoreContext((state) => state.crmOwnerRecordType);
const crmAppSlug = useBookerStore((state) => state.crmAppSlug); const crmAppSlug = useBookerStoreContext((state) => state.crmAppSlug);
const crmRecordId = useBookerStore((state) => state.crmRecordId); const crmRecordId = useBookerStoreContext((state) => state.crmRecordId);
const handleError = (err: any) => { const handleError = (err: any) => {
const errorMessage = err?.message ? t(err.message) : t("can_you_try_again"); const errorMessage = err?.message ? t(err.message) : t("can_you_try_again");
showToast(errorMessage, "error"); showToast(errorMessage, "error");
+5 -5
View File
@@ -2,7 +2,7 @@ import { useEffect } from "react";
import { shallow } from "zustand/shallow"; import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useSlotReservationId } from "@calcom/features/bookings/Booker/useSlotReservationId"; import { useSlotReservationId } from "@calcom/features/bookings/Booker/useSlotReservationId";
import type { BookerEvent } from "@calcom/features/bookings/types"; import type { BookerEvent } from "@calcom/features/bookings/types";
import { MINUTES_TO_BOOK } from "@calcom/lib/constants"; import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
@@ -36,8 +36,8 @@ export const useSlots = (
handleSlotReservation, handleSlotReservation,
}: UseSlotsCallbacks & { isBookingDryRun?: boolean } = {} }: UseSlotsCallbacks & { isBookingDryRun?: boolean } = {}
) => { ) => {
const selectedDuration = useBookerStore((state) => state.selectedDuration); const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
const [selectedTimeslot, setSelectedTimeslot] = useBookerStore( const [selectedTimeslot, setSelectedTimeslot] = useBookerStoreContext(
(state) => [state.selectedTimeslot, state.setSelectedTimeslot], (state) => [state.selectedTimeslot, state.setSelectedTimeslot],
shallow shallow
); );
@@ -52,7 +52,7 @@ export const useSlots = (
onError: onReserveSlotError, onError: onReserveSlotError,
}); });
const seatedEventData = useBookerStore((state) => state.seatedEventData); const seatedEventData = useBookerStoreContext((state) => state.seatedEventData);
const removeSelectedSlot = useDeleteSelectedSlot({ const removeSelectedSlot = useDeleteSelectedSlot({
onSuccess: onDeleteSlotSuccess, onSuccess: onDeleteSlotSuccess,
@@ -85,7 +85,7 @@ export const useSlots = (
} }
}; };
const timeslot = useBookerStore((state) => state.selectedTimeslot); const timeslot = useBookerStoreContext((state) => state.selectedTimeslot);
useEffect(() => { useEffect(() => {
handleReserveSlot(); handleReserveSlot();
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "module", "type": "module",
"description": "Customizable UI components to integrate scheduling into your product.", "description": "Customizable UI components to integrate scheduling into your product.",
"authors": "Cal.com, Inc.", "authors": "Cal.com, Inc.",
"version": "1.3.0", "version": "1.3.1",
"scripts": { "scripts": {
"dev": "yarn vite build --watch & npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify --watch", "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", "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",
@@ -18,7 +18,8 @@ export class CreatePrivateLinkInput {
@IsInt() @IsInt()
@Min(1) @Min(1)
@ApiPropertyOptional({ @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, type: Number,
example: 10, example: 10,
minimum: 1, minimum: 1,
@@ -58,4 +59,25 @@ export class UpdatePrivateLinkInput {
maxUsageCount?: number; 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;
}
@@ -99,6 +99,18 @@ export async function getBookings({
role: { role: {
in: ["ADMIN", "OWNER"], in: ["ADMIN", "OWNER"],
}, },
...(user.orgId && {
OR: [
{
teamId: user.orgId,
},
{
team: {
parentId: user.orgId,
},
},
],
}),
}, },
select: { select: {
id: true, id: true,
@@ -4,6 +4,9 @@ import { sendNoShowFeeChargedEmail } from "@calcom/emails";
import { ErrorCode } from "@calcom/lib/errorCodes"; import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors"; import { ErrorWithCode } from "@calcom/lib/errors";
import { getTranslation } from "@calcom/lib/server/i18n"; 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 { PrismaClient } from "@calcom/prisma";
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar"; import type { CalendarEvent } from "@calcom/types/Calendar";
@@ -20,6 +23,7 @@ interface ChargeCardHandlerOptions {
} }
export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions) => { export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions) => {
const { prisma } = ctx; const { prisma } = ctx;
const teamRepository = new TeamRepository(prisma);
const booking = await prisma.booking.findUnique({ const booking = await prisma.booking.findUnique({
where: { where: {
@@ -84,18 +88,14 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions
}, },
}; };
const idToSearchObject = booking.eventType?.teamId const userId = ctx.user.id;
? { teamId: booking.eventType.teamId } const teamId = booking.eventType?.teamId;
: { userId: ctx.user.id }; const appId = booking.payment[0].appId;
if (booking.eventType?.teamId) { if (teamId) {
const userIsInTeam = await prisma.membership.findUnique({ const userIsInTeam = await MembershipRepository.findUniqueByUserIdAndTeamId({
where: { userId,
userId_teamId: { teamId,
userId: ctx.user.id,
teamId: booking.eventType?.teamId,
},
},
}); });
if (!userIsInTeam) { if (!userIsInTeam) {
@@ -103,16 +103,24 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions
} }
} }
const paymentCredential = await prisma.credential.findFirst({ let paymentCredential = await CredentialRepository.findPaymentCredentialByAppIdAndUserIdOrTeamId({
where: { appId,
...idToSearchObject, userId,
appId: booking.payment[0].appId, teamId,
},
include: {
app: true,
},
}); });
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) { if (!paymentCredential?.app) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid payment credential" }); throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid payment credential" });
} }
+191 -152
View File
@@ -2500,7 +2500,7 @@ __metadata:
"@axiomhq/winston": ^1.2.0 "@axiomhq/winston": ^1.2.0
"@calcom/platform-constants": "*" "@calcom/platform-constants": "*"
"@calcom/platform-enums": "*" "@calcom/platform-enums": "*"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.300" "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.306"
"@calcom/platform-types": "*" "@calcom/platform-types": "*"
"@calcom/platform-utils": "*" "@calcom/platform-utils": "*"
"@calcom/prisma": "*" "@calcom/prisma": "*"
@@ -2590,7 +2590,7 @@ __metadata:
"@sentry/nextjs": ^9.15.0 "@sentry/nextjs": ^9.15.0
bcryptjs: ^2.4.3 bcryptjs: ^2.4.3
memory-cache: ^0.2.0 memory-cache: ^0.2.0
next: ^15.3.0 next: ^15.4.5
next-api-middleware: ^1.0.1 next-api-middleware: ^1.0.1
next-axiom: ^0.17.0 next-axiom: ^0.17.0
next-swagger-doc: ^0.3.6 next-swagger-doc: ^0.3.6
@@ -3559,13 +3559,13 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.300": "@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.306":
version: 0.0.300 version: 0.0.306
resolution: "@calcom/platform-libraries@npm:0.0.300" resolution: "@calcom/platform-libraries@npm:0.0.306"
dependencies: dependencies:
"@calcom/features": "*" "@calcom/features": "*"
"@calcom/lib": "*" "@calcom/lib": "*"
checksum: bff9f08a0586169f2d5a85167f0a0e0e57ca93b637ca858a72b5d3d943ceef37d652efa3d2e6f7561548e4dfe3aabb5c0c6e0a8d38f68d3d21d5e6993e56ad7d checksum: 602b8b446508429096b8e03e9edbe1ff744cf7c7af3fbe8a683aaaf0d3c96d1cd30ff356f72682449ca072beadaae95fdaa5c44b26e30cfce3bb838f519216f9
languageName: node languageName: node
linkType: hard linkType: hard
@@ -4115,7 +4115,7 @@ __metadata:
mime-types: ^2.1.35 mime-types: ^2.1.35
module-alias: ^2.2.2 module-alias: ^2.2.2
msw: ^0.42.3 msw: ^0.42.3
next: ^15.3.0 next: ^15.4.5
next-auth: ^4.22.1 next-auth: ^4.22.1
next-axiom: ^0.17.0 next-axiom: ^0.17.0
next-collect: ^0.2.1 next-collect: ^0.2.1
@@ -4711,12 +4711,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@emnapi/runtime@npm:^1.4.0": "@emnapi/runtime@npm:^1.4.4":
version: 1.4.3 version: 1.4.5
resolution: "@emnapi/runtime@npm:1.4.3" resolution: "@emnapi/runtime@npm:1.4.5"
dependencies: dependencies:
tslib: ^2.4.0 tslib: ^2.4.0
checksum: ff2074809638ed878e476ece370c6eae7e6257bf029a581bb7a290488d8f2a08c420a65988c7f03bfc6bb689218f0cd995d2f935bd182150b357fc2341142f4f checksum: 99ab25d55cf1ceeec12f83b60f48e744f8e1dfc8d52a2ed81b3b09bf15182e61ef55f25b69d51ec83044861bddaa4404e7c3285bf71dd518a7980867e41c2a10
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6724,11 +6724,11 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-darwin-arm64@npm:0.34.1": "@img/sharp-darwin-arm64@npm:0.34.3":
version: 0.34.1 version: 0.34.3
resolution: "@img/sharp-darwin-arm64@npm:0.34.1" resolution: "@img/sharp-darwin-arm64@npm:0.34.3"
dependencies: dependencies:
"@img/sharp-libvips-darwin-arm64": 1.1.0 "@img/sharp-libvips-darwin-arm64": 1.2.0
dependenciesMeta: dependenciesMeta:
"@img/sharp-libvips-darwin-arm64": "@img/sharp-libvips-darwin-arm64":
optional: true optional: true
@@ -6748,11 +6748,11 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-darwin-x64@npm:0.34.1": "@img/sharp-darwin-x64@npm:0.34.3":
version: 0.34.1 version: 0.34.3
resolution: "@img/sharp-darwin-x64@npm:0.34.1" resolution: "@img/sharp-darwin-x64@npm:0.34.3"
dependencies: dependencies:
"@img/sharp-libvips-darwin-x64": 1.1.0 "@img/sharp-libvips-darwin-x64": 1.2.0
dependenciesMeta: dependenciesMeta:
"@img/sharp-libvips-darwin-x64": "@img/sharp-libvips-darwin-x64":
optional: true optional: true
@@ -6767,9 +6767,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-libvips-darwin-arm64@npm:1.1.0": "@img/sharp-libvips-darwin-arm64@npm:1.2.0":
version: 1.1.0 version: 1.2.0
resolution: "@img/sharp-libvips-darwin-arm64@npm:1.1.0" resolution: "@img/sharp-libvips-darwin-arm64@npm:1.2.0"
conditions: os=darwin & cpu=arm64 conditions: os=darwin & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6781,9 +6781,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-libvips-darwin-x64@npm:1.1.0": "@img/sharp-libvips-darwin-x64@npm:1.2.0":
version: 1.1.0 version: 1.2.0
resolution: "@img/sharp-libvips-darwin-x64@npm:1.1.0" resolution: "@img/sharp-libvips-darwin-x64@npm:1.2.0"
conditions: os=darwin & cpu=x64 conditions: os=darwin & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6795,9 +6795,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-libvips-linux-arm64@npm:1.1.0": "@img/sharp-libvips-linux-arm64@npm:1.2.0":
version: 1.1.0 version: 1.2.0
resolution: "@img/sharp-libvips-linux-arm64@npm:1.1.0" resolution: "@img/sharp-libvips-linux-arm64@npm:1.2.0"
conditions: os=linux & cpu=arm64 & libc=glibc conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6809,16 +6809,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-libvips-linux-arm@npm:1.1.0": "@img/sharp-libvips-linux-arm@npm:1.2.0":
version: 1.1.0 version: 1.2.0
resolution: "@img/sharp-libvips-linux-arm@npm:1.1.0" resolution: "@img/sharp-libvips-linux-arm@npm:1.2.0"
conditions: os=linux & cpu=arm & libc=glibc conditions: os=linux & cpu=arm & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-libvips-linux-ppc64@npm:1.1.0": "@img/sharp-libvips-linux-ppc64@npm:1.2.0":
version: 1.1.0 version: 1.2.0
resolution: "@img/sharp-libvips-linux-ppc64@npm:1.1.0" resolution: "@img/sharp-libvips-linux-ppc64@npm:1.2.0"
conditions: os=linux & cpu=ppc64 & libc=glibc conditions: os=linux & cpu=ppc64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6830,9 +6830,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-libvips-linux-s390x@npm:1.1.0": "@img/sharp-libvips-linux-s390x@npm:1.2.0":
version: 1.1.0 version: 1.2.0
resolution: "@img/sharp-libvips-linux-s390x@npm:1.1.0" resolution: "@img/sharp-libvips-linux-s390x@npm:1.2.0"
conditions: os=linux & cpu=s390x & libc=glibc conditions: os=linux & cpu=s390x & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6844,9 +6844,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-libvips-linux-x64@npm:1.1.0": "@img/sharp-libvips-linux-x64@npm:1.2.0":
version: 1.1.0 version: 1.2.0
resolution: "@img/sharp-libvips-linux-x64@npm:1.1.0" resolution: "@img/sharp-libvips-linux-x64@npm:1.2.0"
conditions: os=linux & cpu=x64 & libc=glibc conditions: os=linux & cpu=x64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6858,9 +6858,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-libvips-linuxmusl-arm64@npm:1.1.0": "@img/sharp-libvips-linuxmusl-arm64@npm:1.2.0":
version: 1.1.0 version: 1.2.0
resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.1.0" resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.2.0"
conditions: os=linux & cpu=arm64 & libc=musl conditions: os=linux & cpu=arm64 & libc=musl
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6872,9 +6872,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-libvips-linuxmusl-x64@npm:1.1.0": "@img/sharp-libvips-linuxmusl-x64@npm:1.2.0":
version: 1.1.0 version: 1.2.0
resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.1.0" resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.2.0"
conditions: os=linux & cpu=x64 & libc=musl conditions: os=linux & cpu=x64 & libc=musl
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6891,11 +6891,11 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-linux-arm64@npm:0.34.1": "@img/sharp-linux-arm64@npm:0.34.3":
version: 0.34.1 version: 0.34.3
resolution: "@img/sharp-linux-arm64@npm:0.34.1" resolution: "@img/sharp-linux-arm64@npm:0.34.3"
dependencies: dependencies:
"@img/sharp-libvips-linux-arm64": 1.1.0 "@img/sharp-libvips-linux-arm64": 1.2.0
dependenciesMeta: dependenciesMeta:
"@img/sharp-libvips-linux-arm64": "@img/sharp-libvips-linux-arm64":
optional: true optional: true
@@ -6915,11 +6915,11 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-linux-arm@npm:0.34.1": "@img/sharp-linux-arm@npm:0.34.3":
version: 0.34.1 version: 0.34.3
resolution: "@img/sharp-linux-arm@npm:0.34.1" resolution: "@img/sharp-linux-arm@npm:0.34.3"
dependencies: dependencies:
"@img/sharp-libvips-linux-arm": 1.1.0 "@img/sharp-libvips-linux-arm": 1.2.0
dependenciesMeta: dependenciesMeta:
"@img/sharp-libvips-linux-arm": "@img/sharp-libvips-linux-arm":
optional: true optional: true
@@ -6927,6 +6927,18 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@img/sharp-linux-s390x@npm:0.33.5":
version: 0.33.5 version: 0.33.5
resolution: "@img/sharp-linux-s390x@npm:0.33.5" resolution: "@img/sharp-linux-s390x@npm:0.33.5"
@@ -6939,11 +6951,11 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-linux-s390x@npm:0.34.1": "@img/sharp-linux-s390x@npm:0.34.3":
version: 0.34.1 version: 0.34.3
resolution: "@img/sharp-linux-s390x@npm:0.34.1" resolution: "@img/sharp-linux-s390x@npm:0.34.3"
dependencies: dependencies:
"@img/sharp-libvips-linux-s390x": 1.1.0 "@img/sharp-libvips-linux-s390x": 1.2.0
dependenciesMeta: dependenciesMeta:
"@img/sharp-libvips-linux-s390x": "@img/sharp-libvips-linux-s390x":
optional: true optional: true
@@ -6963,11 +6975,11 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-linux-x64@npm:0.34.1": "@img/sharp-linux-x64@npm:0.34.3":
version: 0.34.1 version: 0.34.3
resolution: "@img/sharp-linux-x64@npm:0.34.1" resolution: "@img/sharp-linux-x64@npm:0.34.3"
dependencies: dependencies:
"@img/sharp-libvips-linux-x64": 1.1.0 "@img/sharp-libvips-linux-x64": 1.2.0
dependenciesMeta: dependenciesMeta:
"@img/sharp-libvips-linux-x64": "@img/sharp-libvips-linux-x64":
optional: true optional: true
@@ -6987,11 +6999,11 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-linuxmusl-arm64@npm:0.34.1": "@img/sharp-linuxmusl-arm64@npm:0.34.3":
version: 0.34.1 version: 0.34.3
resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.1" resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.3"
dependencies: dependencies:
"@img/sharp-libvips-linuxmusl-arm64": 1.1.0 "@img/sharp-libvips-linuxmusl-arm64": 1.2.0
dependenciesMeta: dependenciesMeta:
"@img/sharp-libvips-linuxmusl-arm64": "@img/sharp-libvips-linuxmusl-arm64":
optional: true optional: true
@@ -7011,11 +7023,11 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-linuxmusl-x64@npm:0.34.1": "@img/sharp-linuxmusl-x64@npm:0.34.3":
version: 0.34.1 version: 0.34.3
resolution: "@img/sharp-linuxmusl-x64@npm:0.34.1" resolution: "@img/sharp-linuxmusl-x64@npm:0.34.3"
dependencies: dependencies:
"@img/sharp-libvips-linuxmusl-x64": 1.1.0 "@img/sharp-libvips-linuxmusl-x64": 1.2.0
dependenciesMeta: dependenciesMeta:
"@img/sharp-libvips-linuxmusl-x64": "@img/sharp-libvips-linuxmusl-x64":
optional: true optional: true
@@ -7032,15 +7044,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-wasm32@npm:0.34.1": "@img/sharp-wasm32@npm:0.34.3":
version: 0.34.1 version: 0.34.3
resolution: "@img/sharp-wasm32@npm:0.34.1" resolution: "@img/sharp-wasm32@npm:0.34.3"
dependencies: dependencies:
"@emnapi/runtime": ^1.4.0 "@emnapi/runtime": ^1.4.4
conditions: cpu=wasm32 conditions: cpu=wasm32
languageName: node languageName: node
linkType: hard 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": "@img/sharp-win32-ia32@npm:0.33.5":
version: 0.33.5 version: 0.33.5
resolution: "@img/sharp-win32-ia32@npm:0.33.5" resolution: "@img/sharp-win32-ia32@npm:0.33.5"
@@ -7048,9 +7067,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-win32-ia32@npm:0.34.1": "@img/sharp-win32-ia32@npm:0.34.3":
version: 0.34.1 version: 0.34.3
resolution: "@img/sharp-win32-ia32@npm:0.34.1" resolution: "@img/sharp-win32-ia32@npm:0.34.3"
conditions: os=win32 & cpu=ia32 conditions: os=win32 & cpu=ia32
languageName: node languageName: node
linkType: hard linkType: hard
@@ -7062,9 +7081,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@img/sharp-win32-x64@npm:0.34.1": "@img/sharp-win32-x64@npm:0.34.3":
version: 0.34.1 version: 0.34.3
resolution: "@img/sharp-win32-x64@npm:0.34.1" resolution: "@img/sharp-win32-x64@npm:0.34.3"
conditions: os=win32 & cpu=x64 conditions: os=win32 & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
@@ -8940,10 +8959,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@next/env@npm:15.3.3": "@next/env@npm:15.4.6":
version: 15.3.3 version: 15.4.6
resolution: "@next/env@npm:15.3.3" resolution: "@next/env@npm:15.4.6"
checksum: 52513b04ff1951ad094b37a89dcd37e9275c2d6c635dbe7fbdc44bd39b5511eb2c7c537a80ebbf92d197e5f1293ff16c175cd5dccc9c54bbfaac7271c1fe1bb9 checksum: bf1ea15f2a86c6bc968d92d4fb778d106e6e94c31893f51699f0fc51fd17416ae9e4086176164e82430c84597d509b77c2c5738e8cd98bc7f73ebc6d83b2926f
languageName: node languageName: node
linkType: hard linkType: hard
@@ -8995,9 +9014,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-darwin-arm64@npm:15.3.3": "@next/swc-darwin-arm64@npm:15.4.6":
version: 15.3.3 version: 15.4.6
resolution: "@next/swc-darwin-arm64@npm:15.3.3" resolution: "@next/swc-darwin-arm64@npm:15.4.6"
conditions: os=darwin & cpu=arm64 conditions: os=darwin & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9023,9 +9042,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-darwin-x64@npm:15.3.3": "@next/swc-darwin-x64@npm:15.4.6":
version: 15.3.3 version: 15.4.6
resolution: "@next/swc-darwin-x64@npm:15.3.3" resolution: "@next/swc-darwin-x64@npm:15.4.6"
conditions: os=darwin & cpu=x64 conditions: os=darwin & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9051,9 +9070,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-linux-arm64-gnu@npm:15.3.3": "@next/swc-linux-arm64-gnu@npm:15.4.6":
version: 15.3.3 version: 15.4.6
resolution: "@next/swc-linux-arm64-gnu@npm:15.3.3" resolution: "@next/swc-linux-arm64-gnu@npm:15.4.6"
conditions: os=linux & cpu=arm64 & libc=glibc conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9079,9 +9098,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-linux-arm64-musl@npm:15.3.3": "@next/swc-linux-arm64-musl@npm:15.4.6":
version: 15.3.3 version: 15.4.6
resolution: "@next/swc-linux-arm64-musl@npm:15.3.3" resolution: "@next/swc-linux-arm64-musl@npm:15.4.6"
conditions: os=linux & cpu=arm64 & libc=musl conditions: os=linux & cpu=arm64 & libc=musl
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9107,9 +9126,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-linux-x64-gnu@npm:15.3.3": "@next/swc-linux-x64-gnu@npm:15.4.6":
version: 15.3.3 version: 15.4.6
resolution: "@next/swc-linux-x64-gnu@npm:15.3.3" resolution: "@next/swc-linux-x64-gnu@npm:15.4.6"
conditions: os=linux & cpu=x64 & libc=glibc conditions: os=linux & cpu=x64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9135,9 +9154,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-linux-x64-musl@npm:15.3.3": "@next/swc-linux-x64-musl@npm:15.4.6":
version: 15.3.3 version: 15.4.6
resolution: "@next/swc-linux-x64-musl@npm:15.3.3" resolution: "@next/swc-linux-x64-musl@npm:15.4.6"
conditions: os=linux & cpu=x64 & libc=musl conditions: os=linux & cpu=x64 & libc=musl
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9163,9 +9182,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-win32-arm64-msvc@npm:15.3.3": "@next/swc-win32-arm64-msvc@npm:15.4.6":
version: 15.3.3 version: 15.4.6
resolution: "@next/swc-win32-arm64-msvc@npm:15.3.3" resolution: "@next/swc-win32-arm64-msvc@npm:15.4.6"
conditions: os=win32 & cpu=arm64 conditions: os=win32 & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9205,9 +9224,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@next/swc-win32-x64-msvc@npm:15.3.3": "@next/swc-win32-x64-msvc@npm:15.4.6":
version: 15.3.3 version: 15.4.6
resolution: "@next/swc-win32-x64-msvc@npm:15.3.3" resolution: "@next/swc-win32-x64-msvc@npm:15.4.6"
conditions: os=win32 & cpu=x64 conditions: os=win32 & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
@@ -24262,6 +24281,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "detect-newline@npm:^3.0.0":
version: 3.1.0 version: 3.1.0
resolution: "detect-newline@npm:3.1.0" resolution: "detect-newline@npm:3.1.0"
@@ -35825,29 +35851,27 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"next@npm:^15.3.0": "next@npm:^15.4.5":
version: 15.3.3 version: 15.4.6
resolution: "next@npm:15.3.3" resolution: "next@npm:15.4.6"
dependencies: dependencies:
"@next/env": 15.3.3 "@next/env": 15.4.6
"@next/swc-darwin-arm64": 15.3.3 "@next/swc-darwin-arm64": 15.4.6
"@next/swc-darwin-x64": 15.3.3 "@next/swc-darwin-x64": 15.4.6
"@next/swc-linux-arm64-gnu": 15.3.3 "@next/swc-linux-arm64-gnu": 15.4.6
"@next/swc-linux-arm64-musl": 15.3.3 "@next/swc-linux-arm64-musl": 15.4.6
"@next/swc-linux-x64-gnu": 15.3.3 "@next/swc-linux-x64-gnu": 15.4.6
"@next/swc-linux-x64-musl": 15.3.3 "@next/swc-linux-x64-musl": 15.4.6
"@next/swc-win32-arm64-msvc": 15.3.3 "@next/swc-win32-arm64-msvc": 15.4.6
"@next/swc-win32-x64-msvc": 15.3.3 "@next/swc-win32-x64-msvc": 15.4.6
"@swc/counter": 0.1.3
"@swc/helpers": 0.5.15 "@swc/helpers": 0.5.15
busboy: 1.6.0
caniuse-lite: ^1.0.30001579 caniuse-lite: ^1.0.30001579
postcss: 8.4.31 postcss: 8.4.31
sharp: ^0.34.1 sharp: ^0.34.3
styled-jsx: 5.1.6 styled-jsx: 5.1.6
peerDependencies: peerDependencies:
"@opentelemetry/api": ^1.1.0 "@opentelemetry/api": ^1.1.0
"@playwright/test": ^1.41.2 "@playwright/test": ^1.51.1
babel-plugin-react-compiler: "*" babel-plugin-react-compiler: "*"
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 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 react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
@@ -35882,7 +35906,7 @@ __metadata:
optional: true optional: true
bin: bin:
next: dist/bin/next next: dist/bin/next
checksum: 0e071a67e50b2770d27268b19dbe5a510297cf31fe3035ae169caa7e93f97bcf58c3f533e22fe300c8ad92b87e4f7d807619f7aa3b97a290de7c2b82b1451c4c checksum: 996e28076e30ae1e077219e46d2555f73391ca8132016c83b6096283f638bd6c7be58e2f320d0ef45ac0c18067138d603f900df50517d20ca804f102d2150d8a
languageName: node languageName: node
linkType: hard linkType: hard
@@ -41705,7 +41729,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 7.7.1
resolution: "semver@npm:7.7.1" resolution: "semver@npm:7.7.1"
bin: bin:
@@ -41714,6 +41738,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "semver@npm:~2.3.1":
version: 2.3.2 version: 2.3.2
resolution: "semver@npm:2.3.2" resolution: "semver@npm:2.3.2"
@@ -41989,33 +42022,35 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"sharp@npm:^0.34.1": "sharp@npm:^0.34.3":
version: 0.34.1 version: 0.34.3
resolution: "sharp@npm:0.34.1" resolution: "sharp@npm:0.34.3"
dependencies: dependencies:
"@img/sharp-darwin-arm64": 0.34.1 "@img/sharp-darwin-arm64": 0.34.3
"@img/sharp-darwin-x64": 0.34.1 "@img/sharp-darwin-x64": 0.34.3
"@img/sharp-libvips-darwin-arm64": 1.1.0 "@img/sharp-libvips-darwin-arm64": 1.2.0
"@img/sharp-libvips-darwin-x64": 1.1.0 "@img/sharp-libvips-darwin-x64": 1.2.0
"@img/sharp-libvips-linux-arm": 1.1.0 "@img/sharp-libvips-linux-arm": 1.2.0
"@img/sharp-libvips-linux-arm64": 1.1.0 "@img/sharp-libvips-linux-arm64": 1.2.0
"@img/sharp-libvips-linux-ppc64": 1.1.0 "@img/sharp-libvips-linux-ppc64": 1.2.0
"@img/sharp-libvips-linux-s390x": 1.1.0 "@img/sharp-libvips-linux-s390x": 1.2.0
"@img/sharp-libvips-linux-x64": 1.1.0 "@img/sharp-libvips-linux-x64": 1.2.0
"@img/sharp-libvips-linuxmusl-arm64": 1.1.0 "@img/sharp-libvips-linuxmusl-arm64": 1.2.0
"@img/sharp-libvips-linuxmusl-x64": 1.1.0 "@img/sharp-libvips-linuxmusl-x64": 1.2.0
"@img/sharp-linux-arm": 0.34.1 "@img/sharp-linux-arm": 0.34.3
"@img/sharp-linux-arm64": 0.34.1 "@img/sharp-linux-arm64": 0.34.3
"@img/sharp-linux-s390x": 0.34.1 "@img/sharp-linux-ppc64": 0.34.3
"@img/sharp-linux-x64": 0.34.1 "@img/sharp-linux-s390x": 0.34.3
"@img/sharp-linuxmusl-arm64": 0.34.1 "@img/sharp-linux-x64": 0.34.3
"@img/sharp-linuxmusl-x64": 0.34.1 "@img/sharp-linuxmusl-arm64": 0.34.3
"@img/sharp-wasm32": 0.34.1 "@img/sharp-linuxmusl-x64": 0.34.3
"@img/sharp-win32-ia32": 0.34.1 "@img/sharp-wasm32": 0.34.3
"@img/sharp-win32-x64": 0.34.1 "@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 color: ^4.2.3
detect-libc: ^2.0.3 detect-libc: ^2.0.4
semver: ^7.7.1 semver: ^7.7.2
dependenciesMeta: dependenciesMeta:
"@img/sharp-darwin-arm64": "@img/sharp-darwin-arm64":
optional: true optional: true
@@ -42043,6 +42078,8 @@ __metadata:
optional: true optional: true
"@img/sharp-linux-arm64": "@img/sharp-linux-arm64":
optional: true optional: true
"@img/sharp-linux-ppc64":
optional: true
"@img/sharp-linux-s390x": "@img/sharp-linux-s390x":
optional: true optional: true
"@img/sharp-linux-x64": "@img/sharp-linux-x64":
@@ -42053,11 +42090,13 @@ __metadata:
optional: true optional: true
"@img/sharp-wasm32": "@img/sharp-wasm32":
optional: true optional: true
"@img/sharp-win32-arm64":
optional: true
"@img/sharp-win32-ia32": "@img/sharp-win32-ia32":
optional: true optional: true
"@img/sharp-win32-x64": "@img/sharp-win32-x64":
optional: true optional: true
checksum: ce2798a2d1a4e4fb9bae5642177e4681050897de8c2d6e1783c8c15d8a40cc0e22bf4ce91d1bf355afd133d16d7032fcfdfa2ed3305eab34b7f8de8f61175519 checksum: f1d70751ab15080957f1a5db5583d00b002e644878e3e9a6d00bdbb31255f6e9df218ef82d96d3588b83023681f35bcf03e265a44e214b11306ef3fb439ac04d
languageName: node languageName: node
linkType: hard linkType: hard