Compare commits

...
43 Commits
Author SHA1 Message Date
Anik Dhabal Babu 87f02f5588 fix: Lock time-zone not working properly (#19152)
Post release / release (push) Canceled after 0s
* fix: flaky test organization.spec

* fix
2025-02-07 13:55:58 +00:00
Anik Dhabal Babu 4671ffc4f4 fix (#19156) 2025-02-07 13:54:41 +00:00
Eunjae Lee 6cad7ac1d1 fix: selected filters for numeric and text filters (#19129)
* fix: selected filters for numeric and text filters

* little adjustment
2025-02-07 13:43:25 +00:00
GitHub Actions aff2c7cdb4 chore: release v4.9.8 2025-02-07 13:36:35 +00:00
luzpaz 787ff543df fix: source typo in bookingScenario.ts test (#19162) 2025-02-07 12:56:48 +00:00
luzpaz 01e442df17 fix: source typo in EditUserForm.tsx (#19157) 2025-02-07 12:48:24 +00:00
808de26401 feat: add option to disable sending transcription emails (#19065)
* feat: add option to disable sending transcription emails

* fix: types and tests

* fix: type error

* fix: type err

* fix: type err

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-02-07 12:33:38 +00:00
6ca76707da test: Scenario that replicates the crash in production (#19071)
* Add a case that crashes

* Add proper crash test case

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-02-07 11:20:29 +00:00
Benny Joo e79af63e6c chore: refactor apiRouteMiddleware and add it to some api routes in App Router (#19146)
* chore: remove try and catch from api routes in app router

* refactor

* refactor apiRouteMiddleware

* refactor
2025-02-07 11:04:34 +00:00
Hariom BalharaandHariom Balhara 24a080b988 Add documentation to pass metadata (#19124)
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
2025-02-07 14:40:22 +05:30
luzpaz 8612f77c07 fix: typos in docs/ (#19144)
Found via `codespell -q 3 -S "*.svg,./apps/web/public/static/locales" -L afterall,datea,fo,ist,optionel`
2025-02-07 06:45:34 +00:00
Benny Joo 1babf5bf50 chore: Remove HeadSeo components where not needed (#19151) 2025-02-07 04:11:26 +00:00
Benny Joo e8890156a1 chore: migrate api/cancel to App Router (#19074)
* Add app/api/cancel/route.ts

* remove pages router api/cancel

* refactors needed

* refactor
2025-02-06 23:16:38 +00:00
Benny Joo c9bfd977b1 perf: make metadata generation light for event type single page (#19143)
* add findTitleById method to Repository

* refactor route
2025-02-07 00:26:29 +02:00
Benny Joo 19921ab7c9 chore: migrate api/availability/calendar to App Router (#19099)
* add app/api/availability/calendar/route.ts

* remove pages router api

* refactor
2025-02-06 15:58:05 -05:00
Benny Joo 239c8a6fa9 chore: migrate /api/team/create (#19105) 2025-02-06 15:57:03 -05:00
Udit Takkar cb9dfaa76b fix: logo for org team event types (#19131)
* fix: logo on booking page

* fix: logo for org team event tyoes

* chore
2025-02-06 20:50:15 +05:30
Lauris Skraucis 0b46415856 feat: track who updated routing form (#19079)
* chore: updatedBy App_RoutingForms_Form schema field

* feat: track who updated routing form
2025-02-06 11:52:53 -03:00
sean-brydon 3a522320e7 change error message (#19135) 2025-02-06 14:06:52 +00:00
Joe Au-Yeung 89033c0e9c chore: Salesforce add more logging (#19106)
* Add more logs

* Remove unused method

* Fallback when creating an event

* Type fix

* Type fix

* Type fix

* Add more logging

* More logging
2025-02-06 10:25:38 -03:00
Benny Joo 3972972d57 fix: do not export client components as default in RSC (#19130) 2025-02-06 12:15:37 +00:00
Lauris Skraucis 695d2c0f39 feat: v2 bookings return cancelledBy and rescheduledBy (#19110) 2025-02-06 12:45:13 +02:00
Eunjae Lee 4c32d0c499 fix: check searchParams existence (#19128) 2025-02-06 10:28:46 +00:00
Somay Chauhan 3252efebb1 feat: enable booker atom for non org team events (#19125)
* feat: enable booker atom for non org team events

* Update useTeamEventType.ts
2025-02-06 12:32:05 +05:30
Keith Williams a6fa9d7d60 chore: Trigger Docker release on publish (#19121) 2025-02-05 21:51:33 +00:00
GitHub Actions 38a80c90bb chore: release v4.9.7
Post release / release (push) Canceled after 0s
2025-02-05 21:27:34 +00:00
Morgan 5f897837f2 chore: create event-type input lengthInMinutesOptions (#19114) 2025-02-05 21:39:16 +02:00
Tushar Bhatt 510e73b579 fix: startdate filter and ui sync on bookings page (#19113)
* fix: date-filter issues

* fixed comment

* fixed comment

* removing extra node
2025-02-05 16:03:52 +00:00
Joe Au-YeungandKeith Williams 1787f6d25d Flatmap today's bookings (#19104)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-02-05 14:35:06 +00:00
Somay Chauhan afa83ddb7f fix: add orgId to EventTeamAssignmentTabPlatformWrapper (#19109) 2025-02-05 18:19:31 +05:30
Eunjae Lee 744359dcdf fix: null check (#19111) 2025-02-05 10:27:42 +00:00
Keith WilliamsandPeer Richelsen b7bf58d560 chore: Don't require checks for /help updates (#19089)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-02-05 06:43:43 -03:00
Lauris Skraucis da02cd06d6 feat: v2 organization bookings endpoints (#18875)
* chore: extra team bookings e2e tests

* feat: v2 organizations bookings endpoints

* feat: v2 organizations bookings endpoints

* test

* revert seed.ts change
2025-02-05 10:58:36 +02:00
Morgan 14ec3813ee chore: enable more router props (#19088)
* chore: enable more router props

* chore: expose booker props via router atom
2025-02-05 08:36:34 +00:00
Hariom BalharaandHariom Balhara 288248d94f Prevent xss in preview aand load resources from known URL (#18978)
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
2025-02-05 12:17:56 +05:30
Peer Richelsen 1232eefece feat: attio app (#19102)
* feat: attio app

* added help page

* fixed redirect
2025-02-04 20:45:44 +00:00
Calcom BotandLingo.dev 24abcc3fb2 feat: update translations via @lingodotdev (#19098)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-02-04 19:06:53 +00:00
Eunjae Lee 935e4c52a4 fix: nullish check on insights routing (#19097) 2025-02-04 16:46:01 +00:00
Sanchit Tewariandamrit 9a8ec4d281 fix: Ensure correct fallback to current month when 'month' query param is missing (#18097)
Updated the query parameter handling logic to properly format the 'date' param
as 'YYYY-MM' in case month param is missing.  This resolves an issue where previous slots were not appearing.

Co-authored-by: amrit <iamamrit27@gmail.com>
2025-02-04 20:33:13 +05:30
Morgan 259624d44d fix: do not force cancellation reason for platform (#19087) 2025-02-04 14:55:05 +00:00
Carina WollendorferandCarinaWolli 7e0845ccf4 run cron job every minute (#19086)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-02-04 14:15:22 +00:00
Tushar Bhatt 0522af0c88 fix: empty state search messaging event page (#19064)
* test

* fix: adding empty search headline

* revert a change
2025-02-04 10:14:09 -03:00
Keith Williams f080a59c89 chore: Update CODEOWNERS for Platform and infra-related code (#18938)
* chore: Update CODEOWNERS for Platform and infra-related code

* Added more high-risk files/folders
2025-02-04 08:39:26 -03:00
163 changed files with 2250 additions and 367 deletions
+9
View File
@@ -1,3 +1,6 @@
/apps/api/**/* @calcom/Platform
/deploy/**/* @calcom/Foundation
/infra/**/* @calcom/Foundation
/packages/app-store/applecalendar/**/* @calcom/Foundation
/packages/app-store/caldavcalendar/**/* @calcom/Foundation
/packages/app-store/exchange2013calendar/**/* @calcom/Foundation
@@ -9,3 +12,9 @@
/packages/app-store/larkcalendar/**/* @calcom/Foundation
/packages/app-store/office365calendar/**/* @calcom/Foundation
/packages/app-store/zohocalendar/**/* @calcom/Foundation
/packages/core/getUserAvailability.ts @calcom/Foundation
/packages/features/bookings/lib/handleNewBooking/**/* @calcom/Foundation
/packages/lib/slots.ts @calcom/Foundation
/packages/platform/**/* @calcom/Platform
/packages/prisma/**/* @calcom/Foundation
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
+2 -2
View File
@@ -4,8 +4,8 @@ on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “every 5 minutes” (see https://crontab.guru)
- cron: "*/5 * * * *"
# Runs “every minute” (see https://crontab.guru)
- cron: "* * * * *"
jobs:
cron-webhookTriggers:
env:
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
with:
filters: |
has-files-requiring-all-checks:
- "!(**.md|.github/CODEOWNERS|docs/**|apps/web/public/static/locales/**/common.json)"
- "!(**.md|.github/CODEOWNERS|docs/**|help/**|apps/web/public/static/locales/**/common.json)"
- name: Get Latest Commit SHA
id: get_sha
run: |
+1
View File
@@ -4,6 +4,7 @@ on: # yamllint disable-line rule:truthy
release:
types:
- created
- published
# in case manual trigger is needed
workflow_dispatch:
inputs:
View File
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+1 -1
View File
@@ -30,7 +30,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.93",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.94",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -32,5 +32,6 @@ import { Module } from "@nestjs/common";
ApiKeyRepository,
],
controllers: [BookingsController_2024_08_13],
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
})
export class BookingsModule_2024_08_13 {}
@@ -561,6 +561,43 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should get bookings by teamId and eventTypeId", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamId=${team2.id}&eventTypeId=${team2EventTypeId}`)
.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(1);
expect(data[0].eventTypeId).toEqual(team2EventTypeId);
});
});
it("should not get bookings by teamId and non existing eventTypeId", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamId=${team2.id}&eventTypeId=90909`)
.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(0);
});
});
it("should should get bookings by teamIds", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamIds=${team1.id},${team2.id}`)
@@ -1308,6 +1308,108 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("who cancelled and rescheduled a booking", () => {
it("should return who cancelled a booking", async () => {
const cancelledByEmail = `user-bookings-canceller-${randomString()}@canceller.com`;
const cancelledBooking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${eventTypeId}`,
title: "booking title",
startTime: "2050-09-05T11:00:00.000Z",
endTime: "2050-09-05T12:00:00.000Z",
eventType: {
connect: {
id: eventTypeId,
},
},
status: "CANCELLED",
cancelledBy: cancelledByEmail,
metadata: {},
responses: {
name: "tester",
email: "tester@example.com",
guests: [],
},
user: {
connect: {
id: user.id,
},
},
});
return request(app.getHttpServer())
.get(`/v2/bookings/${cancelledBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.uid).toEqual(cancelledBooking.uid);
expect(data.cancelledByEmail).toEqual(cancelledByEmail);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should return who rescheduled a booking", async () => {
const rescheduledByEmail = `user-bookings-rescheduler-${randomString()}@rescheduler.com`;
const rescheduledBooking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${eventTypeId}`,
title: "booking title",
startTime: "2050-09-05T11:00:00.000Z",
endTime: "2050-09-05T12:00:00.000Z",
eventType: {
connect: {
id: eventTypeId,
},
},
status: "CANCELLED",
rescheduledBy: rescheduledByEmail,
metadata: {},
responses: {
name: "tester",
email: "tester@example.com",
guests: [],
},
user: {
connect: {
id: user.id,
},
},
});
return request(app.getHttpServer())
.get(`/v2/bookings/${rescheduledBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
await bookingsRepositoryFixture.deleteById(rescheduledBooking.id);
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.uid).toEqual(rescheduledBooking.uid);
expect(data.rescheduledByEmail).toEqual(rescheduledByEmail);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
return (
!Array.isArray(data) &&
@@ -108,7 +108,9 @@ export class OutputBookingsService_2024_08_13 {
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
cancelledByEmail: databaseBooking.cancelledBy || undefined,
reschedulingReason: bookingResponses?.rescheduledReason,
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
end: databaseBooking.endTime,
@@ -198,7 +200,9 @@ export class OutputBookingsService_2024_08_13 {
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
cancelledByEmail: databaseBooking.cancelledBy || undefined,
reschedulingReason: bookingResponses?.rescheduledReason,
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
end: databaseBooking.endTime,
@@ -10,8 +10,8 @@ import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { SlotsModule } from "@/modules/slots/slots.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { TeamsModule } from "@/modules/teams/teams/teams.module";
import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module";
import { TeamsModule } from "@/modules/teams/teams/teams.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { Module } from "@nestjs/common";
@@ -4,6 +4,7 @@ import { BillingModule } from "@/modules/billing/billing.module";
import { ConferencingModule } from "@/modules/conferencing/conferencing.module";
import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/controllers/teams/bookings/organizations-teams-bookings.module";
import { RouterModule } from "@/modules/router/router.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { TimezoneModule } from "@/modules/timezones/timezones.module";
@@ -25,6 +26,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
AtomsModule,
StripeModule,
ConferencingModule,
OrganizationsTeamsBookingsModule,
RouterModule,
],
})
@@ -0,0 +1,179 @@
import { ApiProperty } from "@nestjs/swagger";
import { Transform, Type } from "class-transformer";
import {
ArrayMinSize,
ArrayNotEmpty,
IsArray,
IsEnum,
IsInt,
IsISO8601,
IsNumber,
IsOptional,
IsString,
Max,
Min,
} from "class-validator";
enum Status {
upcoming = "upcoming",
recurring = "recurring",
past = "past",
cancelled = "cancelled",
unconfirmed = "unconfirmed",
}
type StatusType = keyof typeof Status;
enum SortOrder {
asc = "asc",
desc = "desc",
}
type SortOrderType = keyof typeof SortOrder;
export class GetOrganizationsTeamsBookingsInput_2024_08_13 {
// note(Lauris): filters
@IsOptional()
@Transform(({ value }) => {
if (typeof value === "string") {
return value.split(",").map((status: string) => status.trim());
}
return value;
})
@ArrayNotEmpty({ message: "status cannot be empty." })
@IsEnum(Status, {
each: true,
message: "Invalid status. Allowed are upcoming, recurring, past, cancelled, unconfirmed",
})
@ApiProperty({
required: false,
description:
"Filter bookings by status. If you want to filter by multiple statuses, separate them with a comma.",
example: "?status=upcoming,past",
enum: Status,
isArray: true,
})
status?: StatusType[];
@IsString()
@IsOptional()
@ApiProperty({
type: String,
required: false,
description: "Filter bookings by the attendee's email address.",
example: "example@domain.com",
})
attendeeEmail?: string;
@IsString()
@IsOptional()
@ApiProperty({
type: String,
required: false,
description: "Filter bookings by the attendee's name.",
example: "John Doe",
})
attendeeName?: string;
@IsOptional()
@Transform(({ value }) => {
if (typeof value === "string") {
return value.split(",").map((eventTypeId: string) => parseInt(eventTypeId));
}
return value;
})
@IsArray()
@IsNumber({}, { each: true })
@ArrayMinSize(1, { message: "eventTypeIds must contain at least 1 event type id" })
@ApiProperty({
type: String,
required: false,
description:
"Filter bookings by event type ids belonging to the team. Event type ids must be separated by a comma.",
example: "?eventTypeIds=100,200",
})
eventTypeIds?: number[];
@IsInt()
@IsOptional()
@Type(() => Number)
@ApiProperty({
type: String,
required: false,
description: "Filter bookings by event type id belonging to the team.",
example: "?eventTypeId=100",
})
eventTypeId?: number;
@IsOptional()
@IsISO8601({ strict: true }, { message: "fromDate must be a valid ISO 8601 date." })
@ApiProperty({
type: String,
required: false,
description: "Filter bookings with start after this date string.",
example: "?afterStart=2025-03-07T10:00:00.000Z",
})
afterStart?: string;
@IsOptional()
@IsISO8601({ strict: true }, { message: "toDate must be a valid ISO 8601 date." })
@ApiProperty({
type: String,
required: false,
description: "Filter bookings with end before this date string.",
example: "?beforeEnd=2025-03-07T11:00:00.000Z",
})
beforeEnd?: string;
// note(Lauris): sort
@IsOptional()
@IsEnum(SortOrder, {
message: 'SortStart must be either "asc" or "desc".',
})
@ApiProperty({
required: false,
description: "Sort results by their start time in ascending or descending order.",
example: "?sortStart=asc OR ?sortStart=desc",
enum: SortOrder,
})
sortStart?: SortOrderType;
@IsOptional()
@IsEnum(SortOrder, {
message: 'SortEnd must be either "asc" or "desc".',
})
@ApiProperty({
required: false,
description: "Sort results by their end time in ascending or descending order.",
example: "?sortEnd=asc OR ?sortEnd=desc",
enum: SortOrder,
})
sortEnd?: SortOrderType;
@IsOptional()
@IsEnum(SortOrder, {
message: 'SortCreated must be either "asc" or "desc".',
})
@ApiProperty({
required: false,
description:
"Sort results by their creation time (when booking was made) in ascending or descending order.",
example: "?sortCreated=asc OR ?sortCreated=desc",
enum: SortOrder,
})
sortCreated?: SortOrderType;
// note(Lauris): pagination
@ApiProperty({ required: false, description: "The number of items to return", example: 10 })
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@Min(1)
@Max(250)
@IsOptional()
take?: number;
@ApiProperty({ required: false, description: "The number of items to skip", example: 0 })
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@Min(0)
@IsOptional()
skip?: number;
}
@@ -0,0 +1,364 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
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 { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/controllers/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 { 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 {
CAL_API_VERSION_HEADER,
SUCCESS_STATUS,
VERSION_2024_08_13,
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
} from "@calcom/platform-constants";
import {
CreateBookingInput_2024_08_13,
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 TeamsBookings Endpoints 2024-08-13", () => {
describe("Organization Team bookings", () => {
let app: INestApplication;
let organization: Team;
let team1: 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;
const teamUserEmail = "orgUser1team1@api.com";
const teamUserEmail2 = "orgUser2team1@api.com";
let teamUser: User;
let teamUser2: User;
let team1EventTypeId: number;
beforeAll(async () => {
const moduleRef = await withApiAuth(
teamUserEmail,
Test.createTestingModule({
imports: [AppModule, OrganizationsTeamsBookingsModule],
})
)
.overrideGuard(PermissionsGuard)
.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);
organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" });
oAuthClient = await createOAuthClient(organization.id);
team1 = await teamRepositoryFixture.create({
name: "team 1",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClient.id,
},
},
});
teamUser = await userRepositoryFixture.create({
email: teamUserEmail,
locale: "it",
name: "orgUser1team1",
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
teamUser2 = await userRepositoryFixture.create({
email: teamUserEmail2,
locale: "es",
name: "orgUser2team1",
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(teamUser.id, userSchedule);
await schedulesService.createUserSchedule(teamUser2.id, userSchedule);
await profileRepositoryFixture.create({
uid: `usr-${teamUser.id}`,
username: teamUserEmail,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: teamUser.id,
},
},
});
await profileRepositoryFixture.create({
uid: `usr-${teamUser2.id}`,
username: teamUserEmail2,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: teamUser2.id,
},
},
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: teamUser.id } },
team: { connect: { id: team1.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: teamUser.id } },
team: { connect: { id: organization.id } },
accepted: true,
});
const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team1.id },
},
title: "Collective Event Type",
slug: "collective-event-type",
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
});
team1EventTypeId = team1EventType.id;
await hostsRepositoryFixture.create({
isFixed: true,
user: {
connect: {
id: teamUser.id,
},
},
eventType: {
connect: {
id: team1EventType.id,
},
},
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
describe("create team bookings", () => {
it("should create a team 1 booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: team1EventTypeId,
attendee: {
name: "alice",
email: "alice@gmail.com",
timeZone: "Europe/Madrid",
language: "es",
},
meetingUrl: "https://meet.google.com/abc-def-ghi",
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts.length).toEqual(1);
expect(data.hosts[0].id).toEqual(teamUser.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString());
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(team1EventTypeId);
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(data.meetingUrl).toEqual(body.meetingUrl);
expect(data.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
describe("get team bookings", () => {
it("should should get bookings by teamId", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${organization.id}/teams/${team1.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(team1EventTypeId);
});
});
it("should get bookings by teamId and eventTypeId", async () => {
return request(app.getHttpServer())
.get(
`/v2/organizations/${organization.id}/teams/${team1.id}/bookings?eventTypeId=${team1EventTypeId}`
)
.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(team1EventTypeId);
});
});
it("should not get bookings by teamId and non existing eventTypeId", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${organization.id}/teams/${team1.id}/bookings?eventTypeId=90909`)
.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(0);
});
});
it("should not get bookings by non existing teamId", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${organization.id}/teams/90909/bookings`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(404);
});
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(teamUser.email);
await userRepositoryFixture.deleteByEmail(teamUserEmail2);
await bookingsRepositoryFixture.deleteAllBookings(teamUser.id, teamUser.email);
await app.close();
});
});
});
@@ -0,0 +1,47 @@
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.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 { GetOrganizationsTeamsBookingsInput_2024_08_13 } from "@/modules/organizations/controllers/teams/bookings/inputs/get-organizations-teams-bookings.input";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query, HttpStatus, HttpCode } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
@Controller({
path: "/v2/organizations/:orgId/teams/:teamId/bookings",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Teams / Bookings")
export class OrganizationsTeamsBookingsController {
constructor(private readonly bookingsService: BookingsService_2024_08_13) {}
@Get("/")
@ApiOperation({ summary: "Get organization team bookings" })
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@HttpCode(HttpStatus.OK)
async getAllOrgTeamBookings(
@Query() queryParams: GetOrganizationsTeamsBookingsInput_2024_08_13,
@Param("teamId", ParseIntPipe) teamId: number,
@GetUser() user: UserWithProfile
): Promise<GetBookingsOutput_2024_08_13> {
const bookings = await this.bookingsService.getBookings({ ...queryParams, teamId }, user);
return {
status: SUCCESS_STATUS,
data: bookings,
};
}
}
@@ -0,0 +1,16 @@
import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OrganizationsTeamsBookingsController } from "@/modules/organizations/controllers/teams/bookings/organizations-teams-bookings.controller";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { Module } from "@nestjs/common";
@Module({
imports: [BookingsModule_2024_08_13, PrismaModule, StripeModule, RedisModule, MembershipsModule],
providers: [OrganizationsRepository, OrganizationsTeamsRepository],
controllers: [OrganizationsTeamsBookingsController],
})
export class OrganizationsTeamsBookingsModule {}
@@ -137,10 +137,14 @@ export class TeamsEventTypesController {
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
): Promise<GetTeamEventTypesOutput> {
const { eventSlug } = queryParams;
const { eventSlug, hostsLimit } = queryParams;
if (eventSlug) {
const eventType = await this.teamsEventTypesService.getTeamEventTypeBySlug(teamId, eventSlug);
const eventType = await this.teamsEventTypesService.getTeamEventTypeBySlug(
teamId,
eventSlug,
hostsLimit
);
return {
status: SUCCESS_STATUS,
+176 -2
View File
@@ -2220,6 +2220,182 @@
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/bookings": {
"get": {
"operationId": "OrganizationsTeamsBookingsController_getAllOrgTeamBookings",
"summary": "Get organization team bookings",
"parameters": [
{
"name": "status",
"required": false,
"in": "query",
"description": "Filter bookings by status. If you want to filter by multiple statuses, separate them with a comma.",
"example": "?status=upcoming,past",
"schema": {
"type": "array",
"items": {
"type": "string",
"enum": [
"upcoming",
"recurring",
"past",
"cancelled",
"unconfirmed"
]
}
}
},
{
"name": "attendeeEmail",
"required": false,
"in": "query",
"description": "Filter bookings by the attendee's email address.",
"example": "example@domain.com",
"schema": {
"type": "string"
}
},
{
"name": "attendeeName",
"required": false,
"in": "query",
"description": "Filter bookings by the attendee's name.",
"example": "John Doe",
"schema": {
"type": "string"
}
},
{
"name": "eventTypeIds",
"required": false,
"in": "query",
"description": "Filter bookings by event type ids belonging to the team. Event type ids must be separated by a comma.",
"example": "?eventTypeIds=100,200",
"schema": {
"type": "string"
}
},
{
"name": "eventTypeId",
"required": false,
"in": "query",
"description": "Filter bookings by event type id belonging to the team.",
"example": "?eventTypeId=100",
"schema": {
"type": "string"
}
},
{
"name": "afterStart",
"required": false,
"in": "query",
"description": "Filter bookings with start after this date string.",
"example": "?afterStart=2025-03-07T10:00:00.000Z",
"schema": {
"type": "string"
}
},
{
"name": "beforeEnd",
"required": false,
"in": "query",
"description": "Filter bookings with end before this date string.",
"example": "?beforeEnd=2025-03-07T11:00:00.000Z",
"schema": {
"type": "string"
}
},
{
"name": "sortStart",
"required": false,
"in": "query",
"description": "Sort results by their start time in ascending or descending order.",
"example": "?sortStart=asc OR ?sortStart=desc",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "sortEnd",
"required": false,
"in": "query",
"description": "Sort results by their end time in ascending or descending order.",
"example": "?sortEnd=asc OR ?sortEnd=desc",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "sortCreated",
"required": false,
"in": "query",
"description": "Sort results by their creation time (when booking was made) in ascending or descending order.",
"example": "?sortCreated=asc OR ?sortCreated=desc",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "The number of items to return",
"example": 10,
"schema": {
"minimum": 1,
"maximum": 250,
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "The number of items to skip",
"example": 0,
"schema": {
"minimum": 0,
"type": "number"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetBookingsOutput_2024_08_13"
}
}
}
}
},
"tags": [
"Orgs / Teams / Bookings"
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
"get": {
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
@@ -8516,7 +8692,6 @@
},
"required": [
"lengthInMinutes",
"lengthInMinutesOptions",
"title",
"slug"
]
@@ -12010,7 +12185,6 @@
},
"required": [
"lengthInMinutes",
"lengthInMinutesOptions",
"title",
"slug",
"schedulingType",
@@ -19,7 +19,10 @@ export class OrganizationRepositoryFixture {
async create(data: Prisma.TeamCreateInput) {
return await this.prismaWriteClient.$transaction(async (prisma) => {
const team = await prisma.team.create({
data,
data: {
...data,
isOrganization: true,
},
});
await prisma.organizationSettings.create({
@@ -9,4 +9,8 @@ export const generateMetadata = async () => {
);
};
export default VerifyEmailPage;
const ServerPageWrapper = async () => {
return <VerifyEmailPage />;
};
export default ServerPageWrapper;
@@ -2,19 +2,43 @@ import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { z } from "zod";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
import { buildLegacyCtx, buildLegacyRequest } from "@lib/buildLegacyCtx";
import type { PageProps as EventTypePageProps } from "@lib/event-types/[type]/getServerSideProps";
import { getServerSideProps } from "@lib/event-types/[type]/getServerSideProps";
import EventTypePageWrapper from "~/event-types/views/event-types-single-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const { eventType } = await getData(legacyCtx);
const querySchema = z.object({
type: z
.string()
.refine((val) => !isNaN(Number(val)), {
message: "event-type id must be a string that can be cast to a number",
})
.transform((val) => Number(val)),
});
export const generateMetadata = async ({ params }: _PageProps) => {
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
const parsed = querySchema.safeParse(params);
if (!parsed.success || !session?.user?.id) {
return await _generateMetadata(
(t) => `${t("event_type")}`,
() => ""
);
}
const data = await EventTypeRepository.findTitleById({
id: parsed.data.type,
userId: session?.user?.id,
});
return await _generateMetadata(
(t) => `${eventType.title} | ${t("event_type")}`,
(t) => (data?.title ? `${data.title} | ${t("event_type")}` : `${t("event_type")}`),
() => ""
);
};
@@ -9,4 +9,8 @@ export const generateMetadata = async () => {
);
};
export default Page;
const ServerPageWrapper = async () => {
return <Page />;
};
export default ServerPageWrapper;
@@ -1,4 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
@@ -6,10 +8,11 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
import { HttpError } from "@calcom/lib/http-error";
import notEmpty from "@calcom/lib/notEmpty";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const selectedCalendarSelectSchema = z.object({
integration: z.string(),
externalId: z.string(),
@@ -17,9 +20,8 @@ const selectedCalendarSelectSchema = z.object({
eventTypeId: z.coerce.number().nullish(),
});
/** Shared authentication middleware for GET, DELETE and POST requests */
async function authMiddleware(req: CustomNextApiRequest) {
const session = await getServerSession({ req });
async function authMiddleware() {
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
if (!session?.user?.id) {
throw new HttpError({ statusCode: 401, message: "Not authenticated" });
@@ -30,56 +32,14 @@ async function authMiddleware(req: CustomNextApiRequest) {
if (!userWithCredentials) {
throw new HttpError({ statusCode: 401, message: "Not authenticated" });
}
req.userWithCredentials = userWithCredentials;
return userWithCredentials;
}
type CustomNextApiRequest = NextApiRequest & {
userWithCredentials?: Awaited<ReturnType<typeof authMiddleware>>;
};
async function postHandler(req: CustomNextApiRequest) {
if (!req.userWithCredentials) throw new HttpError({ statusCode: 401, message: "Not authenticated" });
const user = req.userWithCredentials;
const { integration, externalId, credentialId, eventTypeId } = selectedCalendarSelectSchema.parse(req.body);
await SelectedCalendarRepository.upsert({
userId: user.id,
integration,
externalId,
credentialId,
eventTypeId: eventTypeId ?? null,
});
return { message: "Calendar Selection Saved" };
}
async function deleteHandler(req: CustomNextApiRequest) {
if (!req.userWithCredentials) throw new HttpError({ statusCode: 401, message: "Not authenticated" });
const user = req.userWithCredentials;
const { integration, externalId, credentialId, eventTypeId } = selectedCalendarSelectSchema.parse(
req.query
);
const calendarCacheRepository = await CalendarCache.initFromCredentialId(credentialId);
await calendarCacheRepository.unwatchCalendar({
calendarId: externalId,
eventTypeIds: [eventTypeId ?? null],
});
await SelectedCalendarRepository.delete({
where: {
userId: user.id,
externalId,
integration,
eventTypeId: eventTypeId ?? null,
},
});
return { message: "Calendar Selection Saved" };
}
// TODO: It doesn't seem to be used from within the app. It is possible that someone outside Cal.com is using this GET endpoint
async function getHandler(req: CustomNextApiRequest) {
if (!req.userWithCredentials) throw new HttpError({ statusCode: 401, message: "Not authenticated" });
const user = req.userWithCredentials;
async function getHandler() {
const user = await authMiddleware();
const selectedCalendarIds = await SelectedCalendarRepository.findMany({
where: { userId: user.id },
select: { externalId: true },
@@ -91,18 +51,54 @@ async function getHandler(req: CustomNextApiRequest) {
calendarCredentials,
user.userLevelSelectedCalendars
);
const calendars = connectedCalendars.flatMap((c) => c.calendars).filter(notEmpty);
const selectableCalendars = calendars.map((cal) => {
return { selected: selectedCalendarIds.findIndex((s) => s.externalId === cal.externalId) > -1, ...cal };
});
return selectableCalendars;
return NextResponse.json(selectableCalendars);
}
export default defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
await authMiddleware(req);
return defaultHandler({
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
DELETE: Promise.resolve({ default: defaultResponder(deleteHandler) }),
})(req, res);
});
async function postHandler(req: NextRequest) {
const user = await authMiddleware();
const body = await req.json();
const { integration, externalId, credentialId, eventTypeId } = selectedCalendarSelectSchema.parse(body);
await SelectedCalendarRepository.upsert({
userId: user.id,
integration,
externalId,
credentialId,
eventTypeId: eventTypeId ?? null,
});
return NextResponse.json({ message: "Calendar Selection Saved" });
}
async function deleteHandler(req: NextRequest) {
const user = await authMiddleware();
const searchParams = Object.fromEntries(req.nextUrl.searchParams.entries());
const { integration, externalId, credentialId, eventTypeId } =
selectedCalendarSelectSchema.parse(searchParams);
const calendarCacheRepository = await CalendarCache.initFromCredentialId(credentialId);
await calendarCacheRepository.unwatchCalendar({
calendarId: externalId,
eventTypeIds: [eventTypeId ?? null],
});
await SelectedCalendarRepository.delete({
where: {
userId: user.id,
externalId,
integration,
eventTypeId: eventTypeId ?? null,
},
});
return NextResponse.json({ message: "Calendar Selection Saved" });
}
export { deleteHandler as DELETE, postHandler as POST, getHandler as GET };
+32
View File
@@ -0,0 +1,32 @@
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
import { apiRouteMiddleware } from "@calcom/lib/server/apiRouteMiddleware";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
async function handler(req: NextRequest) {
let appDirRequestBody;
try {
appDirRequestBody = await req.json();
} catch (error) {
return NextResponse.json({ success: false, message: "Invalid JSON" }, { status: 400 });
}
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
const result = await handleCancelBooking({
appDirRequestBody,
userId: session?.user?.id || -1,
});
const statusCode = result.success ? 200 : 400;
return NextResponse.json(result, { status: statusCode });
}
const deleteHandler = apiRouteMiddleware((req: NextRequest) => handler(req));
const postHandler = apiRouteMiddleware((req: NextRequest) => handler(req));
export { deleteHandler as DELETE, postHandler as POST };
+3 -2
View File
@@ -1,6 +1,7 @@
import { cardExamples } from "@pages/api/plain/example-cards";
import { createHmac } from "crypto";
import { headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
@@ -26,7 +27,7 @@ const inputSchema = z.object({
cardKeys: z.array(z.string()),
});
async function handler(request: Request) {
async function handler(request: NextRequest) {
const headersList = headers();
const requestBody = await request.json();
@@ -38,7 +39,7 @@ async function handler(request: Request) {
.digest("hex");
if (incomingSignature !== expectedSignature) {
return new Response("Forbidden", { status: 403 });
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
// Validate request body
+4 -3
View File
@@ -1,4 +1,5 @@
import { createHmac } from "crypto";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
@@ -14,16 +15,16 @@ const responseSchema = z.object({
chatAvatarUrl: z.string(),
});
async function handler(request: Request) {
async function handler(request: NextRequest) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const session = await getServerSession({ req: request as any });
if (!session?.user?.email) {
return new Response("Unauthorized - No session email found", { status: 401 });
return NextResponse.json({ error: "Unauthorized - No session email found" }, { status: 401 });
}
const secret = process.env.PLAIN_CHAT_HMAC_SECRET_KEY;
if (!secret) {
return new Response("Missing Plain Chat secret", { status: 500 });
return NextResponse.json({ error: "Missing Plain Chat secret" }, { status: 500 });
}
const hmac = createHmac("sha256", secret);
@@ -1,10 +1,10 @@
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import type Stripe from "stripe";
import { z } from "zod";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
@@ -22,8 +22,11 @@ const generateRandomString = () => {
return Math.random().toString(36).substring(2, 10);
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { session_id } = querySchema.parse(req.query);
async function getHandler(req: NextRequest) {
const searchParams = req.nextUrl.searchParams;
const { session_id } = querySchema.parse({
session_id: searchParams.get("session_id"),
});
const checkoutSession = await stripe.checkout.sessions.retrieve(session_id, {
expand: ["subscription"],
@@ -81,9 +84,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
});
// redirect to team screen
res.redirect(302, `/settings/teams/${team.id}/onboard-members?event=team_created`);
return NextResponse.redirect(
new URL(`/settings/teams/${team.id}/onboard-members?event=team_created`, req.nextUrl.origin),
{ status: 302 }
);
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});
export { getHandler as GET };
-6
View File
@@ -1,7 +1,6 @@
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui";
import type { AppPageProps } from "./AppPage";
import { AppPage } from "./AppPage";
@@ -14,11 +13,6 @@ const ShellHeading = () => {
export default function WrappedApp(props: AppPageProps) {
return (
<Shell smallHeading isPublic hideHeadingOnMobile heading={<ShellHeading />} backPath="/apps" withoutSeo>
<HeadSeo
title={props.name}
description={props.description}
app={{ slug: props.logo, name: props.name, description: props.description }}
/>
{props.licenseRequired ? (
<LicenseRequired>
<AppPage {...props} />
@@ -88,6 +88,7 @@ const getEventTypes = async (userId: number, teamIds?: number[]) => {
position: true,
recurringEvent: true,
requiresConfirmation: true,
canSendCalVideoTranscriptionEmails: true,
team: { select: { slug: true } },
schedulingType: true,
teamId: true,
+1
View File
@@ -30,6 +30,7 @@ export const getEventTypesFromDB = async (id: number) => {
eventName: true,
recurringEvent: true,
requiresConfirmation: true,
canSendCalVideoTranscriptionEmails: true,
userId: true,
successRedirectUrl: true,
customInputs: true,
+1
View File
@@ -21,6 +21,7 @@ export const getBooking = async (bookingId: number) => {
select: {
teamId: true,
parentId: true,
canSendCalVideoTranscriptionEmails: true,
},
},
user: {
@@ -199,7 +199,7 @@ function BookingsContent({ status }: BookingsProps) {
const bookingsToday = useMemo<RowData[]>(() => {
return (
query.data?.pages.map((page) =>
query.data?.pages.flatMap((page) =>
page.bookings
.filter(
(booking: BookingOutput) =>
@@ -214,7 +214,7 @@ function BookingsContent({ status }: BookingsProps) {
),
isToday: true,
}))
)[0] || []
) || []
);
}, [query.data]);
@@ -457,9 +457,9 @@ export const InfiniteEventTypeList = ({
if (isPending) return <InfiniteSkeletonLoader />;
return group.teamId ? (
<EmptyEventTypeList group={group} />
<EmptyEventTypeList group={group} searchTerm={debouncedSearchTerm} />
) : !group.profile.eventTypesLockedByOrg ? (
<CreateFirstEventTypeView slug={group.profile.slug ?? ""} />
<CreateFirstEventTypeView slug={group.profile.slug ?? ""} searchTerm={debouncedSearchTerm} />
) : (
<></>
);
@@ -818,13 +818,13 @@ export const InfiniteEventTypeList = ({
);
};
const CreateFirstEventTypeView = ({ slug }: { slug: string }) => {
const CreateFirstEventTypeView = ({ slug, searchTerm }: { slug: string; searchTerm?: string }) => {
const { t } = useLocale();
return (
<EmptyScreen
Icon="link"
headline={t("new_event_type_heading")}
headline={searchTerm ? t("no_result_found_for", { searchTerm }) : t("new_event_type_heading")}
description={t("new_event_type_description")}
className="mb-16"
buttonRaw={
@@ -863,12 +863,18 @@ const CTA = ({
);
};
const EmptyEventTypeList = ({ group }: { group: EventTypeGroup | InfiniteEventTypeGroup }) => {
const EmptyEventTypeList = ({
group,
searchTerm,
}: {
group: EventTypeGroup | InfiniteEventTypeGroup;
searchTerm?: string;
}) => {
const { t } = useLocale();
return (
<>
<EmptyScreen
headline={t("team_no_event_types")}
headline={searchTerm ? t("no_result_found_for", { searchTerm }) : t("team_no_event_types")}
buttonRaw={
<Button
href={`?dialog=new&eventPage=${group.profile.slug}&teamId=${group.teamId}`}
+1 -12
View File
@@ -39,17 +39,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import {
Button,
HeadSeo,
PasswordField,
TextField,
Form,
Alert,
CheckboxField,
Icon,
showToast,
} from "@calcom/ui";
import { Button, PasswordField, TextField, Form, Alert, CheckboxField, Icon, showToast } from "@calcom/ui";
import type { getServerSideProps } from "@lib/signup/getServerSideProps";
@@ -337,7 +327,6 @@ export default function Signup({
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1] "
)}>
<div className="bg-muted 2xl:border-subtle grid w-full max-w-[1440px] grid-cols-1 grid-rows-1 overflow-hidden lg:grid-cols-2 2xl:rounded-[20px] 2xl:border 2xl:py-6">
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
{/* Left side */}
<div className="ml-auto mr-auto mt-0 flex w-full max-w-xl flex-col px-4 pt-6 sm:px-16 md:px-20 lg:mt-24 2xl:px-28">
{displayBackButton && (
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "4.9.6",
"version": "4.9.8",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
-17
View File
@@ -1,17 +0,0 @@
import type { NextApiRequest } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
import { defaultResponder, defaultHandler } from "@calcom/lib/server";
async function handler(req: NextApiRequest & { userId?: number }) {
const session = await getServerSession({ req });
/* To mimic API behavior and comply with types */
req.userId = session?.user?.id || -1;
return await handleCancelBooking(req);
}
export default defaultHandler({
DELETE: Promise.resolve({ default: defaultResponder(handler) }),
POST: Promise.resolve({ default: defaultResponder(handler) }),
});
@@ -158,6 +158,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const bookingReference = await getBookingReference(room);
const booking = await getBooking(bookingReference.bookingId as number);
if (!booking.eventType?.canSendCalVideoTranscriptionEmails) {
return res.status(200).json({
message: `Transcription emails are disabled for this event type ${booking.eventTypeId}`,
});
}
const transcripts = await getAllTranscriptsAccessLinkFromMeetingId(meeting_id);
if (!transcripts || !transcripts.length)
@@ -108,6 +108,7 @@
"accept_license": "اقبل الترخيص",
"still_waiting_for_approval": "يوجد حدث لا يزال في انتظار الموافقة",
"event_is_still_waiting": "لا يزال طلب الحدث في الانتظار: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "لا توجد نتائج لـ \"{{searchTerm}}\"",
"no_more_results": "لا يوجد مزيد من النتائج",
"no_results": "لا نتائج",
"load_more_results": "تحميل المزيد من النتائج",
@@ -108,6 +108,7 @@
"accept_license": "Lisenziyanı qəbul edin",
"still_waiting_for_approval": "Tədbir hələ də təsdiq gözləyir",
"event_is_still_waiting": "Tədbir sorğusu hələ də gözləyir: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "\"{{searchTerm}}\" üçün heç bir nəticə tapılmadı",
"no_more_results": "Daha çox nəticə yoxdur",
"no_results": "Nəticə yoxdur",
"load_more_results": "Daha çox nəticə yüklə",
@@ -108,6 +108,7 @@
"accept_license": "Приемам лиценза",
"still_waiting_for_approval": "Все още има събитие, очакващо одобрение",
"event_is_still_waiting": "Заявката за събитие все още чака: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Няма намерени резултати за \"{{searchTerm}}\"",
"no_more_results": "Няма повече резултати",
"no_results": "Няма резултати",
"load_more_results": "Зареди още резултати",
@@ -108,6 +108,7 @@
"accept_license": "Accepta la llicència",
"still_waiting_for_approval": "Un esdeveniment encara està pendent d'aprovació",
"event_is_still_waiting": "La sol·licitud d'esdeveniment encara està pendent: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "No s'han trobat resultats per a \"{{searchTerm}}\"",
"no_more_results": "No hi ha més resultats",
"no_results": "No hi ha resultats",
"load_more_results": "Carrega més resultats",
@@ -108,6 +108,7 @@
"accept_license": "Přijmout licenci",
"still_waiting_for_approval": "Událost stále čeká na schválení",
"event_is_still_waiting": "Požadavek na událost stále čeká: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Pro \"{{searchTerm}}\" nebyly nalezeny žádné výsledky",
"no_more_results": "Žádné další výsledky",
"no_results": "Žádné výsledky",
"load_more_results": "Načíst další výsledky",
@@ -108,6 +108,7 @@
"accept_license": "Acceptér Licens",
"still_waiting_for_approval": "En begivenhed venter stadig på godkendelse",
"event_is_still_waiting": "Begivenhedsforespørgsel venter stadig: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Ingen resultater fundet for \"{{searchTerm}}\"",
"no_more_results": "Ikke flere resultater",
"no_results": "Ingen resultater",
"load_more_results": "Indlæs flere resultater",
@@ -108,6 +108,7 @@
"accept_license": "Lizenz akzeptieren",
"still_waiting_for_approval": "Ein Termin wartet noch auf Bestätigung",
"event_is_still_waiting": "Termin-Anfrage wartet: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Keine Ergebnisse gefunden für \"{{searchTerm}}\"",
"no_more_results": "Keine weiteren Ergebnisse",
"no_results": "Keine Ergebnisse",
"load_more_results": "Weitere Ergebnisse laden",
@@ -108,6 +108,7 @@
"accept_license": "Αποδοχή Άδειας",
"still_waiting_for_approval": "Ένα συμβάν εξακολουθεί να περιμένει έγκριση",
"event_is_still_waiting": "Το αίτημα εκδήλωσης εξακολουθεί να περιμένει: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Δεν βρέθηκαν αποτελέσματα για \"{{searchTerm}}\"",
"no_more_results": "Δεν υπάρχουν άλλα αποτελέσματα",
"no_results": "Δεν υπάρχουν αποτελέσματα",
"load_more_results": "Φόρτωση περισσότερων αποτελεσμάτων",
@@ -24,6 +24,8 @@
"verify_email_email_header": "Verify your email address",
"verify_email_email_button": "Verify email",
"cal_ai_assistant": "Cal AI Assistant",
"send_cal_video_transcription_emails": "Send Cal Video Transcription Emails",
"description_send_cal_video_transcription_emails": "Send emails with the transcription of the Cal Video after the meeting ends. (Requires a paid plan)",
"verify_email_change_description": "You have recently requested to change the email address you use to log into your {{appName}} account. Please click the button below to confirm your new email address.",
"verify_email_change_success_toast": "Updated your email to {{email}}",
"verify_email_change_failure_toast": "Failed to update email.",
@@ -108,6 +110,7 @@
"accept_license": "Accept License",
"still_waiting_for_approval": "An event is still waiting for approval",
"event_is_still_waiting": "Event request is still waiting: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "No result found for \"{{searchTerm}}\"",
"no_more_results": "No more results",
"no_results": "No results",
"load_more_results": "Load more results",
@@ -108,6 +108,7 @@
"accept_license": "Aceptar licencia",
"still_waiting_for_approval": "Un evento aún está esperando aprobación",
"event_is_still_waiting": "La solicitud de evento aún está esperando: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "No se encontraron resultados para \"{{searchTerm}}\"",
"no_more_results": "No hay más resultados",
"no_results": "No hay resultados",
"load_more_results": "Cargar más resultados",
@@ -108,6 +108,7 @@
"accept_license": "Aceptar licencia",
"still_waiting_for_approval": "Un evento aún está esperando su aprobación",
"event_is_still_waiting": "La solicitud del evento todavía está esperando: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "No se encontraron resultados para \"{{searchTerm}}\"",
"no_more_results": "No hay más resultados",
"no_results": "Sin resultados",
"load_more_results": "Cargar más resultados",
@@ -108,6 +108,7 @@
"accept_license": "Nõustu litsentsiga",
"still_waiting_for_approval": "Üritus ootab endiselt heakskiitu",
"event_is_still_waiting": "Ürituse taotlus on endiselt ootel: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Otsingusõnale \"{{searchTerm}}\" ei leitud ühtegi tulemust",
"no_more_results": "Enam tulemusi pole",
"no_results": "Tulemused puuduvad",
"load_more_results": "Lae rohkem tulemusi",
@@ -108,6 +108,7 @@
"accept_license": "Onart Lizentzia",
"still_waiting_for_approval": "Gertaera bat onarpenaren zain dago",
"event_is_still_waiting": "Gertaera-eskaera oraindik zain dago: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Ez da emaitzarik aurkitu \"{{searchTerm}}\" bilaketarako",
"no_more_results": "Emaitza gehiagorik ez",
"no_results": "Emaitzarik ez",
"load_more_results": "Kargatu emaitza gehiago",
@@ -108,6 +108,7 @@
"accept_license": "Hyväksy lisenssi",
"still_waiting_for_approval": "Tapahtuma odottaa edelleen hyväksyntää",
"event_is_still_waiting": "Tapahtumapyyntö odottaa edelleen: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Ei tuloksia hakusanalle \"{{searchTerm}}\"",
"no_more_results": "Ei enempää tuloksia",
"no_results": "Ei tuloksia",
"load_more_results": "Lataa lisää tuloksia",
@@ -108,6 +108,7 @@
"accept_license": "Accepter la licence",
"still_waiting_for_approval": "Un événement est toujours en attente de validation",
"event_is_still_waiting": "La demande d'événement est toujours en attente : {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Aucun résultat trouvé pour \"{{searchTerm}}\"",
"no_more_results": "Il n'y a plus de résultats",
"no_results": "Aucun résultat",
"load_more_results": "Charger plus de résultats",
@@ -108,6 +108,7 @@
"accept_license": "אישור הרישיון",
"still_waiting_for_approval": "אירוע עדיין ממתין לאישור",
"event_is_still_waiting": "בקשת האירוע עדיין בהמתנה: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "לא נמצאו תוצאות עבור \"{{searchTerm}}\"",
"no_more_results": "אין יותר תוצאות",
"no_results": "אין תוצאות",
"load_more_results": "לטעון תוצאות נוספות",
@@ -108,6 +108,7 @@
"accept_license": "Licensz elfogadása",
"still_waiting_for_approval": "Egy esemény még jóváhagyásra vár",
"event_is_still_waiting": "Még mindig függőben lévő kérés: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Nincs találat a következőre: \"{{searchTerm}}\"",
"no_more_results": "Nincs több eredmény",
"no_results": "Nincs eredmény",
"load_more_results": "További eredmények betöltése",
@@ -108,6 +108,7 @@
"accept_license": "Accetta Licenza",
"still_waiting_for_approval": "Un evento è ancora in attesa di approvazione",
"event_is_still_waiting": "La richiesta d'evento è ancora in attesa: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Nessun risultato trovato per \"{{searchTerm}}\"",
"no_more_results": "Nessun altro risultato",
"no_results": "Nessun risultato",
"load_more_results": "Carica più risultati",
@@ -108,6 +108,7 @@
"accept_license": "ライセンスに同意する",
"still_waiting_for_approval": "イベントはまだ承認待ちです",
"event_is_still_waiting": "イベントリクエストはまだ待機中です: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "\"{{searchTerm}}\"の検索結果が見つかりませんでした",
"no_more_results": "他の結果はありません",
"no_results": "結果はありません",
"load_more_results": "さらに結果を読み込む",
@@ -108,6 +108,7 @@
"accept_license": "ទទួលយកអាជ្ញាប័ណ្ណ",
"still_waiting_for_approval": "ព្រឹត្តិការណ៍មួយនៅតែរង់ចាំការយល់ព្រម",
"event_is_still_waiting": "សំណើព្រឹត្តិការណ៍កំពុងរង់ចាំ៖ {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "មិនមានលទ្ធផលសម្រាប់ \"{{searchTerm}}\" ទេ",
"no_more_results": "មិនមានលទ្ធផលទៀតទេ",
"no_results": "គ្មាន​លទ្ធផល",
"load_more_results": "ផ្ទុកលទ្ធផលបន្ថែមទៀត",
@@ -108,6 +108,7 @@
"accept_license": "라이선스 승인",
"still_waiting_for_approval": "이벤트가 아직 승인을 기다리고 있습니다.",
"event_is_still_waiting": "이벤트 요청이 아직 대기중입니다: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "\"{{searchTerm}}\"에 대한 검색 결과가 없습니다",
"no_more_results": "더 이상 결과 없음",
"no_results": "결과 없음",
"load_more_results": "더 많은 결과 보기",
@@ -108,6 +108,7 @@
"accept_license": "Licentie accepteren",
"still_waiting_for_approval": "Er is nog een openstaande goedkeuring voor een evenement",
"event_is_still_waiting": "Openstaand verzoek voor evenement: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Geen resultaten gevonden voor \"{{searchTerm}}\"",
"no_more_results": "Geen verdere resultaten",
"no_results": "Geen resultaten",
"load_more_results": "Meer resultaten laden",
@@ -108,6 +108,7 @@
"accept_license": "Godta lisens",
"still_waiting_for_approval": "En hendelse venter fremdeles på godkjenning",
"event_is_still_waiting": "Hendelsesforespørsel venter fortsatt: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Ingen resultater funnet for \"{{searchTerm}}\"",
"no_more_results": "Ingen flere resultater",
"no_results": "Ingen resultater",
"load_more_results": "Last inn flere resultater",
@@ -108,6 +108,7 @@
"accept_license": "Akceptuj licencję",
"still_waiting_for_approval": "Wydarzenie nadal oczekuje na zatwierdzenie",
"event_is_still_waiting": "Prośba o wydarzenie nadal czeka: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Nie znaleziono wyników dla \"{{searchTerm}}\"",
"no_more_results": "Brak więcej wyników",
"no_results": "Brak wyników",
"load_more_results": "Wczytaj więcej wyników",
@@ -108,6 +108,7 @@
"accept_license": "Aceitar licença",
"still_waiting_for_approval": "Um evento ainda aguarda aprovação",
"event_is_still_waiting": "Solicitação de evento pendente: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Nenhum resultado encontrado para \"{{searchTerm}}\"",
"no_more_results": "Não existem mais resultados",
"no_results": "Nenhum resultado",
"load_more_results": "Carregar mais resultados",
@@ -108,6 +108,7 @@
"accept_license": "Aceitar licença",
"still_waiting_for_approval": "Um evento ainda aguarda aprovação",
"event_is_still_waiting": "Pedido de evento pendente: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Nenhum resultado encontrado para \"{{searchTerm}}\"",
"no_more_results": "Não existem mais resultados",
"no_results": "Sem resultados",
"load_more_results": "Carregar mais resultados",
@@ -108,6 +108,7 @@
"accept_license": "Îmi dau acordul pentru Licență",
"still_waiting_for_approval": "Un eveniment încă așteaptă aprobare",
"event_is_still_waiting": "Solicitarea evenimentului este încă în așteptare: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Nu s-au găsit rezultate pentru \"{{searchTerm}}\"",
"no_more_results": "Nu mai există rezultate",
"no_results": "Niciun rezultat",
"load_more_results": "Încărcați mai multe rezultate",
@@ -108,6 +108,7 @@
"accept_license": "Принять лицензию",
"still_waiting_for_approval": "Событие все еще ждет одобрения",
"event_is_still_waiting": "Запрос встречи все еще ожидает: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Ничего не найдено по запросу \"{{searchTerm}}\"",
"no_more_results": "Ничего не найдено",
"no_results": "Нет результатов",
"load_more_results": "Загрузить больше результатов",
@@ -108,6 +108,7 @@
"accept_license": "Prijať licenciu",
"still_waiting_for_approval": "Udalosť stále čaká na schválenie",
"event_is_still_waiting": "Žiadosť o udalosť stále čaká: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Pre výraz \"{{searchTerm}}\" neboli nájdené žiadne výsledky",
"no_more_results": "Žiadne ďalšie výsledky",
"no_results": "Žiadne výsledky",
"load_more_results": "Načítať ďalšie výsledky",
@@ -108,6 +108,7 @@
"accept_license": "Prihvati Licencu",
"still_waiting_for_approval": "Dogadjaj još čeka na odobrenje",
"event_is_still_waiting": "Zahtev na dogadjaj još čeka: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Nema rezultata za \"{{searchTerm}}\"",
"no_more_results": "Nema više rezultata",
"no_results": "Nema rezultata",
"load_more_results": "Učitaj više rezultata",
@@ -108,6 +108,7 @@
"accept_license": "Godkänn licens",
"still_waiting_for_approval": "En bokning väntar fortfarande på godkännande",
"event_is_still_waiting": "Bokningsförfrågan väntar fortfarande: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Inga resultat hittades för \"{{searchTerm}}\"",
"no_more_results": "Inga fler resultat",
"no_results": "Inga resultat",
"load_more_results": "Hämta fler resultat",
@@ -108,6 +108,7 @@
"accept_license": "Lisansı Kabul Et",
"still_waiting_for_approval": "Bir etkinlik hâlâ onay bekliyor",
"event_is_still_waiting": "Etkinlik isteği hâlâ beklemede: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "\"{{searchTerm}}\" için sonuç bulunamadı",
"no_more_results": "Başka sonuç yok",
"no_results": "Sonuç yok",
"load_more_results": "Daha fazla sonuç yükle",
@@ -108,6 +108,7 @@
"accept_license": "Прийняти ліцензію",
"still_waiting_for_approval": "Захід досі чекає на схвалення",
"event_is_still_waiting": "Запит на захід досі в очікуванні: {{attendeeName}} – {{date}} – {{eventType}}",
"no_result_found_for": "За запитом \"{{searchTerm}}\" нічого не знайдено",
"no_more_results": "Інших результатів немає",
"no_results": "Немає результатів",
"load_more_results": "Завантажити інші результати",
@@ -108,6 +108,7 @@
"accept_license": "Chấp nhận giấy phép",
"still_waiting_for_approval": "Một sự kiện vẫn đang chờ được chấp thuận",
"event_is_still_waiting": "Lời mời sự kiện vẫn đang chờ: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "Không tìm thấy kết quả nào cho \"{{searchTerm}}\"",
"no_more_results": "Không còn kết quả",
"no_results": "Không có kết quả",
"load_more_results": "Tải thêm kết quả",
@@ -108,6 +108,7 @@
"accept_license": "接受许可协议",
"still_waiting_for_approval": "有活动正在等待您的批准",
"event_is_still_waiting": "活动请求仍在等待中: {{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "未找到与\"{{searchTerm}}\"相关的结果",
"no_more_results": "无更多结果",
"no_results": "无结果",
"load_more_results": "加载更多结果",
@@ -108,6 +108,7 @@
"accept_license": "接受授權",
"still_waiting_for_approval": "活動還在等待核准",
"event_is_still_waiting": "活動請求還在等待中:{{attendeeName}} - {{date}} - {{eventType}}",
"no_result_found_for": "找不到與「{{searchTerm}}」相關的結果",
"no_more_results": "沒有更多結果",
"no_results": "沒有任何結果",
"load_more_results": "讀取更多結果",
@@ -64,6 +64,7 @@ type UserPageProps = {
| "hidden"
| "lockTimeZoneToggleOnBookingPage"
| "requiresConfirmation"
| "canSendCalVideoTranscriptionEmails"
| "requiresBookerEmailVerification"
| "price"
| "currency"
+71
View File
@@ -1753,6 +1753,77 @@ describe("getSchedule", () => {
}
);
});
test("Edge Case: Same day OOO entries that could accidentally be considered in past shouldn't crash the getSchedule", async () => {
// Set a fixed date for testing
vi.setSystemTime("2024-05-21T00:00:13Z");
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: todayDateString } = getDate({ dateIncrement: 0 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
await createBookingScenario({
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
},
],
users: [
{
...TestData.users.example,
id: 101,
schedules: [TestData.schedules.IstWorkHours],
outOfOffice: {
dateRanges: [
{
start: `${todayDateString}T00:00:00.000Z`,
end: `${todayDateString}T23:59:59.999Z`,
},
],
},
},
],
});
const schedule = await getSchedule({
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
orgSlug: null,
},
});
// Verify that we get slots for the requested dates
expect(schedule).toHaveTimeSlots(
[
"04:00:00.000Z",
"04:45:00.000Z",
"05:30:00.000Z",
"06:15:00.000Z",
"07:00:00.000Z",
"07:45:00.000Z",
"08:30:00.000Z",
"09:15:00.000Z",
"10:00:00.000Z",
"10:45:00.000Z",
"11:30:00.000Z",
],
{
dateString: plus2DateString,
}
);
});
});
describe("Team Event", () => {
@@ -166,6 +166,12 @@ type InputUser = Omit<typeof TestData.users.example, "defaultScheduleId"> & {
weekStart?: string;
profiles?: Prisma.ProfileUncheckedCreateWithoutUserInput[];
completedOnboarding?: boolean;
outOfOffice?: {
dateRanges: {
start: string;
end: string;
}[];
};
};
export type InputEventType = {
@@ -271,7 +277,7 @@ async function addHostsToDb(eventTypes: InputEventType[]) {
export async function addEventTypesToDb(
eventTypes: (Omit<
Prisma.EventTypeCreateInput,
"users" | "worflows" | "destinationCalendar" | "schedule"
"users" | "workflows" | "destinationCalendar" | "schedule"
> & {
id?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -652,14 +658,33 @@ export async function addWorkflowReminders(workflowReminders: InputWorkflowRemin
});
}
export async function addUsersToDb(
users: (Prisma.UserCreateInput & { schedules: Prisma.ScheduleCreateInput[]; id?: number })[]
) {
export async function addUsersToDb(users: InputUser[]) {
log.silly("TestData: Creating Users", JSON.stringify(users));
await prismock.user.createMany({
data: users,
});
// Create OutOfOfficeEntry for users with outOfOffice data
for (const user of users) {
if (user.outOfOffice) {
log.debug("Creating OutOfOfficeEntry for user", user.id);
for (const dateRange of user.outOfOffice.dateRanges) {
await prismock.outOfOfficeEntry.create({
data: {
uuid: uuidv4(),
start: new Date(dateRange.start),
end: new Date(dateRange.end),
user: {
connect: {
id: user.id,
},
},
},
});
}
}
}
const allUsers = await prismock.user.findMany({
include: {
credentials: true,
+2 -2
View File
@@ -3208,7 +3208,7 @@
"tags": ["Memberships"],
"responses": {
"201": {
"description": "OK, membership removed successfuly"
"description": "OK, membership removed successfully"
},
"400": {
"description": "Bad request. Membership id is invalid."
@@ -4538,7 +4538,7 @@
"tags": ["Users"],
"responses": {
"201": {
"description": "OK, user removed successfuly"
"description": "OK, user removed successfully"
},
"400": {
"description": "Bad request. User id is invalid."
+186 -9
View File
@@ -1189,6 +1189,15 @@
"schema": {
"type": "string"
}
},
{
"name": "hostsLimit",
"required": false,
"in": "query",
"description": "Specifies the maximum number of hosts to include in the response. This limit helps optimize performance. If not provided, all Hosts will be fetched.",
"schema": {
"type": "number"
}
}
],
"responses": {
@@ -2097,6 +2106,165 @@
"tags": ["Orgs / Teams"]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/bookings": {
"get": {
"operationId": "OrganizationsTeamsBookingsController_getAllOrgTeamBookings",
"summary": "Get organization team bookings",
"parameters": [
{
"name": "status",
"required": false,
"in": "query",
"description": "Filter bookings by status. If you want to filter by multiple statuses, separate them with a comma.",
"example": "?status=upcoming,past",
"schema": {
"type": "array",
"items": {
"type": "string",
"enum": ["upcoming", "recurring", "past", "cancelled", "unconfirmed"]
}
}
},
{
"name": "attendeeEmail",
"required": false,
"in": "query",
"description": "Filter bookings by the attendee's email address.",
"example": "example@domain.com",
"schema": {
"type": "string"
}
},
{
"name": "attendeeName",
"required": false,
"in": "query",
"description": "Filter bookings by the attendee's name.",
"example": "John Doe",
"schema": {
"type": "string"
}
},
{
"name": "eventTypeIds",
"required": false,
"in": "query",
"description": "Filter bookings by event type ids belonging to the team. Event type ids must be separated by a comma.",
"example": "?eventTypeIds=100,200",
"schema": {
"type": "string"
}
},
{
"name": "eventTypeId",
"required": false,
"in": "query",
"description": "Filter bookings by event type id belonging to the team.",
"example": "?eventTypeId=100",
"schema": {
"type": "string"
}
},
{
"name": "afterStart",
"required": false,
"in": "query",
"description": "Filter bookings with start after this date string.",
"example": "?afterStart=2025-03-07T10:00:00.000Z",
"schema": {
"type": "string"
}
},
{
"name": "beforeEnd",
"required": false,
"in": "query",
"description": "Filter bookings with end before this date string.",
"example": "?beforeEnd=2025-03-07T11:00:00.000Z",
"schema": {
"type": "string"
}
},
{
"name": "sortStart",
"required": false,
"in": "query",
"description": "Sort results by their start time in ascending or descending order.",
"example": "?sortStart=asc OR ?sortStart=desc",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
},
{
"name": "sortEnd",
"required": false,
"in": "query",
"description": "Sort results by their end time in ascending or descending order.",
"example": "?sortEnd=asc OR ?sortEnd=desc",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
},
{
"name": "sortCreated",
"required": false,
"in": "query",
"description": "Sort results by their creation time (when booking was made) in ascending or descending order.",
"example": "?sortCreated=asc OR ?sortCreated=desc",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "The number of items to return",
"example": 10,
"schema": {
"minimum": 1,
"maximum": 250,
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "The number of items to skip",
"example": 0,
"schema": {
"minimum": 0,
"type": "number"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetBookingsOutput_2024_08_13"
}
}
}
}
},
"tags": ["Orgs / Teams / Bookings"]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
"get": {
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
@@ -5582,6 +5750,15 @@
"schema": {
"type": "string"
}
},
{
"name": "hostsLimit",
"required": false,
"in": "query",
"description": "Specifies the maximum number of hosts to include in the response. This limit helps optimize performance. If not provided, all Hosts will be fetched.",
"schema": {
"type": "number"
}
}
],
"responses": {
@@ -7130,7 +7307,7 @@
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' radio buttom will be selected and the select field will be disabled."
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' radio button will be selected and the select field will be disabled."
},
"hidden": {
"type": "boolean",
@@ -7738,7 +7915,7 @@
},
"beforeEventBuffer": {
"type": "number",
"description": "Time spaces that can be pre-pended before an event to give more time before it."
"description": "Time spaces that can be prepended before an event to give more time before it."
},
"afterEventBuffer": {
"type": "number",
@@ -7868,7 +8045,7 @@
"example": "https://masterchief.com/argentina/flan/video/9129412"
}
},
"required": ["lengthInMinutes", "lengthInMinutesOptions", "title", "slug"]
"required": ["lengthInMinutes", "title", "slug"]
},
"OutputAddressLocation_2024_06_14": {
"type": "object",
@@ -8789,7 +8966,7 @@
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' radio buttom will be selected and the select field will be disabled."
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' radio button will be selected and the select field will be disabled."
},
"hidden": {
"type": "boolean",
@@ -9430,7 +9607,7 @@
},
"beforeEventBuffer": {
"type": "number",
"description": "Time spaces that can be pre-pended before an event to give more time before it."
"description": "Time spaces that can be prepended before an event to give more time before it."
},
"afterEventBuffer": {
"type": "number",
@@ -10785,7 +10962,7 @@
},
"beforeEventBuffer": {
"type": "number",
"description": "Time spaces that can be pre-pended before an event to give more time before it."
"description": "Time spaces that can be prepended before an event to give more time before it."
},
"afterEventBuffer": {
"type": "number",
@@ -10928,7 +11105,7 @@
"description": "If true, all current and future team members will be assigned to this event type"
}
},
"required": ["lengthInMinutes", "lengthInMinutesOptions", "title", "slug", "schedulingType", "hosts"]
"required": ["lengthInMinutes", "title", "slug", "schedulingType", "hosts"]
},
"CreateTeamEventTypeOutput": {
"type": "object",
@@ -11556,7 +11733,7 @@
},
"beforeEventBuffer": {
"type": "number",
"description": "Time spaces that can be pre-pended before an event to give more time before it."
"description": "Time spaces that can be prepended before an event to give more time before it."
},
"afterEventBuffer": {
"type": "number",
@@ -15082,7 +15259,7 @@
"externalId": {
"type": "string",
"example": "https://caldav.icloud.com/26962146906/calendars/1644422A-1945-4438-BBC0-4F0Q23A57R7S/",
"description": "Unique identifier used to represent the specfic calendar, as returned by the /calendars endpoint"
"description": "Unique identifier used to represent the specific calendar, as returned by the /calendars endpoint"
}
},
"required": ["integration", "externalId"]
@@ -13,7 +13,7 @@ Contributions are what make the open source community such an amazing place to l
| :-------------------------------------------------------------- | :-------------------------------------------------- |
| Minor improvements, non-core feature requests | <img className="m-0" noZoom src="/images/low-priority.svg" /> |
| Confusing UX (... but working) | <img className="m-0" noZoom src="/images/medium-priority.svg" /> |
| Core Features (Booking page, availabilty, timezone calculation) | <img className="m-0" noZoom src="/images/high-priority.svg" /> |
| Core Features (Booking page, availability, timezone calculation)| <img className="m-0" noZoom src="/images/high-priority.svg" /> |
| Core Bugs (Login, Booking page, Emails are not working) | <img className="m-0" noZoom src="/images/urgent-priority.svg" /> |
+1 -1
View File
@@ -43,7 +43,7 @@ If you would like to get right into exploring our core features which are availa
## Enterprise Features
Cal.com offers a variety of premium features which can be accessed through aquiring the enterprise license. To get an idea of what you get in return for the enterprise license, you can head to our Enterprise Features section:
Cal.com offers a variety of premium features which can be accessed through acquiring the enterprise license. To get an idea of what you get in return for the enterprise license, you can head to our Enterprise Features section:
<div className="flex">
<Button size="smb" variant="pill" href="/docs/enterprise-features/teams">
@@ -78,7 +78,7 @@ We offer all kinds of customizations to the Outlook calendar connect via props.
| loadingLabel | No | Label to display when atom is in loading state |
| onCheckError | No | A callback function to handle errors when checking the connection status |
| initialData | No | Initial data to be passed |
| isMultiCalendar | No | Specfies if the button supports integration for multiple users |
| isMultiCalendar | No | Specifies if the button supports integration for multiple users |
| tooltip | No | In case user wants to pass external tooltip component |
| tooltipSide | No | Specifies what direction the tooltip appears |
| isClickable | No | Boolean to disable button or not |
+1 -1
View File
@@ -240,7 +240,7 @@ Below video shows a demonstration of the all the options you get in the advanced
**6. Recurring**
The recurring tab lets you set up a recurring event. You can set up recurring events such as weekly, monthly, yearly, etc for a maximun of events you want it to repeat.
The recurring tab lets you set up a recurring event. You can set up recurring events such as weekly, monthly, yearly, etc for a maximum of events you want it to repeat.
<img src="/images/Recurring.png" alt="Recurring tab" />
@@ -77,7 +77,7 @@ We offer all kinds of customizations to the Google calendar connect via props. B
| onCheckError | No | A callback function to handle errors when checking the connection status |
| redir | No | A custom redirect URL link where the user gets redirected after successful authentication |
| initialData | No | Initial data to be passed |
| isMultiCalendar | No | Specfies if the button supports integration for multiple users |
| isMultiCalendar | No | Specifies if the button supports integration for multiple users |
| tooltip | No | In case user wants to pass external tooltip component |
| tooltipSide | No | Specifies what direction the tooltip appears |
| isClickable | No | Boolean to disable button or not |
@@ -76,7 +76,7 @@ We offer all kinds of customizations to the Outlook calendar connect via props.
| onCheckError | No | A callback function to handle errors when checking the connection status |
| redir | No | A custom redirect URL link where the user gets redirected after successful authentication |
| initialData | No | Initial data to be passed |
| isMultiCalendar | No | Specfies if the button supports integration for multiple users |
| isMultiCalendar | No | Specifies if the button supports integration for multiple users |
| tooltip | No | In case user wants to pass external tooltip component |
| tooltipSide | No | Specifies what direction the tooltip appears |
| isClickable | No | Boolean to disable button or not |
+1 -1
View File
@@ -69,7 +69,7 @@ export default function EventType(id: number) {
With this you're all set to accept payments for your bookings.
</Step>
<Step title="Use booker atom to access payment uid">
The booker atom has a prop called `onCreateBookingSuccess` which is a callback function that gets called at the time of a successful booking creation. This function takes a parameter called `data` which contains the payment uid and another property called paymentRequired which can be used to detect if the booking requres payment or not. You can use the payment uid to access the payment form atom.
The booker atom has a prop called `onCreateBookingSuccess` which is a callback function that gets called at the time of a successful booking creation. This function takes a parameter called `data` which contains the payment uid and another property called paymentRequired which can be used to detect if the booking requires payment or not. You can use the payment uid to access the payment form atom.
Either store the payment uid in your database, a local state variable or pass it into another page as query parameters.
+2 -2
View File
@@ -27,7 +27,7 @@ description: Answers to the most common questions about the Platform API and ato
- An atom is a customizable UI component that handles scheduling on behalf of your user.
- Everything from the front-end to API calls is being handled by the atom, all you need to is import the atom and drop it in your code.
### 6. What are the minimun setup requirments for using atoms?
### 6. What are the minimum setup requirements for using atoms?
- You need to have a project that uses React version 18 and above.
- At the moment atoms are only supported in React, so there's no way to use them in vanilla js or any other popular framework.
@@ -40,7 +40,7 @@ description: Answers to the most common questions about the Platform API and ato
<html dir="ltr"> ... </html>
```
### 9. What are the authentification methods for making calls to the v2 API?
### 9. What are the authentication methods for making calls to the v2 API?
Either of the two:
- Setting `Authorization: Bearer <token>` header where `<token>` is access token of the managed user.
- Setting `x-cal-client-id: <client-id>` and `x-cal-secret-key: <client-secret>` where `<client-id>` is OAuth client id and `<client-secret>` is OAuth client secret. Both can be copied from the dashboard.
+1 -1
View File
@@ -13,7 +13,7 @@ yarn workspace @calcom/prisma db-deploy
```
command to update the database.
We use database migrations in order to handle changes to the database schema in a more secure and stable way. This is actually very common. The thing is that when just changing the schema in `schema.prisma` without creating migrations, the update to the newer database schema can damage or delete all data in production mode, since the system sometimes doesn't know how to transform the data from A to B. Using migrations, each step is reproducable, transparent and can be undone in a simple way.
We use database migrations in order to handle changes to the database schema in a more secure and stable way. This is actually very common. The thing is that when just changing the schema in `schema.prisma` without creating migrations, the update to the newer database schema can damage or delete all data in production mode, since the system sometimes doesn't know how to transform the data from A to B. Using migrations, each step is reproducible, transparent and can be undone in a simple way.
### Creating migrations
+9
View File
@@ -0,0 +1,9 @@
---
title: "How to Cal.com with Attio"
---
To use Cal.com with Attio, you only need two things:
1. You need to have an account on both platforms
2. You need to connect Google Calendar to Attio
3. You need to connect Google Calendar to Cal.com
+20 -4
View File
@@ -7,7 +7,7 @@ Booking form inside an embed can be prefilled as well.
## Inline Embed
```js
// Prefill Name, Email and Attendee Address - React.
// Prefill Name, Email, Attendee Address and passing metadata - React.
// You can specify config property to do prefilling
// React Demo Link - https://codesandbox.io/s/react-prefill-attendee-address-email-sqhf7r
<Cal
@@ -15,6 +15,10 @@ Booking form inside an embed can be prefilled as well.
config={{
name: "Name",
email: "booker@example.org",
// You will receive the value in payload.metadata["myKey"] in webhook.
// Also, it would be stored in booking table under metadata column
// It won't be shown on the booking details page.
"metadata[myKey]": "myValue",
location: JSON.stringify({
value: "attendeeInPerson",
// It can be any string that defines an address where the meeting would occur
@@ -25,13 +29,17 @@ Booking form inside an embed can be prefilled as well.
```
```js
// Prefill Name, Email and Attendee Address - Vanilla JS
// Prefill Name, Email, Attendee Address and passing metadata - Vanilla JS
// You can use 'inline' instruction to do the prefill
// VanillaJS Demo link - https://codesandbox.io/s/prefill-embed-fields-vanilla-js-zft44w?file=/index.html
Cal("inline", {
config: {
name: "Name",
email: "booker@example.org",
// You will receive the value in payload.metadata["myKey"] in webhook.
// Also, it would be stored in booking table under metadata column
// It won't be shown on the booking details page.
"metadata[myKey]": "myValue",
location: JSON.stringify({
value: "attendeeInPerson",
// It can be any string that defines an address where the meeting would occur
@@ -42,12 +50,16 @@ Cal("inline", {
```
```js
// Prefill Name, Email and Attendee Phonenumber - React
// Prefill Name, Email, Attendee Phonenumber and passing metadata - React
<Cal
// .... other props in here
config={{
name: "Name",
email: "booker@example.org",
// You will receive the value in payload.metadata["myKey"] in webhook.
// Also, it would be stored in booking table under metadata column
// It won't be shown on the booking details page.
"metadata[myKey]": "myValue",
location: JSON.stringify({
value: "phone",
// Any valid phonenumber in here
@@ -76,7 +88,7 @@ Cal("inline", {
## Floating button popup
```js
// Prefill Name, Email and Attendee Phone - Vanilla JS
// Prefill Name, Email, Attendee Phone and passing metadata - Vanilla JS
// You can use 'floatingButton' instruction to do the prefill
// This is common for React and VanillaJS embeds.
// Note that for React, Cal is not Cal component here it is Cal API.
@@ -84,6 +96,10 @@ Cal("floatingButton", {
config: {
name: "Name",
email: "booker@example.org",
// You will receive the value in payload.metadata["myKey"] in webhook.
// Also, it would be stored in booking table under metadata column
// It won't be shown on the booking details page.
"metadata[myKey]": "myValue",
location: JSON.stringify({
value: "phone",
// Any valid phonenumber in here
+2 -1
View File
@@ -158,7 +158,8 @@
"apps-and-integrations/assign-people",
"apps-and-integrations/build-greeter-app",
"apps-and-integrations/how-to-build-an-app",
"apps-and-integrations/syncing-third-party"
"apps-and-integrations/syncing-third-party",
"apps-and-integrations/attio"
]
},
{
+1
View File
@@ -110,6 +110,7 @@ checksums:
accept_license: 1a83a54fdc1248f7a8f48386020e7c98
still_waiting_for_approval: 9a2d726b0c04f779cdea51bc2a41365a
event_is_still_waiting: ff866d6d3fbe18bdacdc79956c6adbbb
no_result_found_for: ba54128bee4b5bc59760ea5ef5c0dc34
no_more_results: 1adb2c45a869a814fa64ed45fe7ff3ce
no_results: 0e9b73265c6542240f5a3bf6b43e9280
load_more_results: 0ca3ac0fd1450284ce8dbd609e3b5b56
+1 -1
View File
@@ -65,7 +65,7 @@
"tdd": "vitest watch",
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
"e2e:embed": "QUICK=true yarn playwright test --project=@calcom/embed-core",
"e2e:embed": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/embed-core",
"e2e:embed-react": "QUICK=true yarn playwright test --project=@calcom/embed-react",
"test-e2e": "yarn db-seed && yarn e2e",
"test-e2e:app-store": "yarn db-seed && yarn e2e:app-store",
@@ -6,6 +6,7 @@ import alby_config_json from "./alby/config.json";
import amie_config_json from "./amie/config.json";
import { metadata as applecalendar__metadata_ts } from "./applecalendar/_metadata";
import around_config_json from "./around/config.json";
import attio_config_json from "./attio/config.json";
import autocheckin_config_json from "./autocheckin/config.json";
import baa_for_hipaa_config_json from "./baa-for-hipaa/config.json";
import basecamp3_config_json from "./basecamp3/config.json";
@@ -109,6 +110,7 @@ export const appStoreMetadata = {
amie: amie_config_json,
applecalendar: applecalendar__metadata_ts,
around: around_config_json,
attio: attio_config_json,
autocheckin: autocheckin_config_json,
"baa-for-hipaa": baa_for_hipaa_config_json,
basecamp3: basecamp3_config_json,
@@ -7,6 +7,7 @@ export const apiHandlers = {
amie: import("./amie/api"),
applecalendar: import("./applecalendar/api"),
around: import("./around/api"),
attio: import("./attio/api"),
autocheckin: import("./autocheckin/api"),
"baa-for-hipaa": import("./baa-for-hipaa/api"),
basecamp3: import("./basecamp3/api"),
+14
View File
@@ -0,0 +1,14 @@
---
items:
- iframe: { src: https://www.youtube.com/embed/bAm5qHUOFes }
- 1.jpeg
- 2.jpeg
- 3.jpeg
- 4.jpeg
- 5.jpeg
- 6.jpeg
---
# Customer relationship magic.
Attio is the AI-native CRM that builds, scales and grows your company to the next level.
+20
View File
@@ -0,0 +1,20 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
redirect: {
newTab: false,
url: "/apps/installed/crm",
},
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
};
export default handler;
+1
View File
@@ -0,0 +1 @@
export { default as add } from "./add";

Some files were not shown because too many files have changed in this diff Show More