Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87f02f5588 | ||
|
|
4671ffc4f4 | ||
|
|
6cad7ac1d1 | ||
|
|
aff2c7cdb4 | ||
|
|
787ff543df | ||
|
|
01e442df17 | ||
|
|
808de26401 | ||
|
|
6ca76707da | ||
|
|
e79af63e6c | ||
|
|
24a080b988 | ||
|
|
8612f77c07 | ||
|
|
1babf5bf50 | ||
|
|
e8890156a1 | ||
|
|
c9bfd977b1 | ||
|
|
19921ab7c9 | ||
|
|
239c8a6fa9 | ||
|
|
cb9dfaa76b | ||
|
|
0b46415856 | ||
|
|
3a522320e7 | ||
|
|
89033c0e9c | ||
|
|
3972972d57 | ||
|
|
695d2c0f39 | ||
|
|
4c32d0c499 | ||
|
|
3252efebb1 | ||
|
|
a6fa9d7d60 | ||
|
|
38a80c90bb | ||
|
|
5f897837f2 | ||
|
|
510e73b579 | ||
|
|
1787f6d25d | ||
|
|
afa83ddb7f | ||
|
|
744359dcdf | ||
|
|
b7bf58d560 | ||
|
|
da02cd06d6 | ||
|
|
14ec3813ee | ||
|
|
288248d94f | ||
|
|
1232eefece | ||
|
|
24abcc3fb2 | ||
|
|
935e4c52a4 | ||
|
|
9a8ec4d281 | ||
|
|
259624d44d | ||
|
|
7e0845ccf4 | ||
|
|
0522af0c88 | ||
|
|
f080a59c89 |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -4,6 +4,7 @@ on: # yamllint disable-line rule:truthy
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
- published
|
||||
# in case manual trigger is needed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
|
||||
Vendored
Vendored
+8
@@ -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"
|
||||
@@ -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,
|
||||
],
|
||||
})
|
||||
|
||||
+179
@@ -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;
|
||||
}
|
||||
+364
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
+47
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+16
@@ -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 {}
|
||||
+6
-2
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
+56
-60
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
|
||||
@@ -30,6 +30,7 @@ export const getEventTypesFromDB = async (id: number) => {
|
||||
eventName: true,
|
||||
recurringEvent: true,
|
||||
requiresConfirmation: true,
|
||||
canSendCalVideoTranscriptionEmails: true,
|
||||
userId: true,
|
||||
successRedirectUrl: true,
|
||||
customInputs: true,
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.9.6",
|
||||
"version": "4.9.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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" /> |
|
||||
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Binary file not shown.
@@ -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"),
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user