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