Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca19aa00c1 | ||
|
|
467c979aa7 | ||
|
|
9f9bf10040 | ||
|
|
8102192987 | ||
|
|
232f9d288c | ||
|
|
7dab100382 | ||
|
|
91d4f1c219 | ||
|
|
a64e7f9c2a | ||
|
|
3c1297aa72 | ||
|
|
96f7a01c5f | ||
|
|
a4a05878ef | ||
|
|
352d68ae1c | ||
|
|
ffd034679c | ||
|
|
d64ae3c901 | ||
|
|
7250e22257 | ||
|
|
f459e5d9c4 | ||
|
|
30bb0f1383 | ||
|
|
24c993a3d2 | ||
|
|
aa4276be5b | ||
|
|
413aba6e52 | ||
|
|
c2547acb3d | ||
|
|
cc1b6bac4a | ||
|
|
d5d90a8b87 | ||
|
|
40333c81c5 | ||
|
|
ff92979311 | ||
|
|
f63edef48d | ||
|
|
37e56ebbf9 | ||
|
|
07c38f1f33 | ||
|
|
192238f4bf | ||
|
|
94a26031a8 | ||
|
|
e9f20b1a0b | ||
|
|
7cb9e2b99b | ||
|
|
dd45d8cdbc | ||
|
|
3a181f1dac | ||
|
|
c3abfa51ba | ||
|
|
6498af167a | ||
|
|
30dce7bacd | ||
|
|
3be9e1bd38 | ||
|
|
db52f44293 | ||
|
|
20d6eb5200 | ||
|
|
424d94f16d | ||
|
|
73e7d6f986 | ||
|
|
145001d26e | ||
|
|
5d2d7ce979 | ||
|
|
4dc21ab2b0 | ||
|
|
df6ee73e05 | ||
|
|
587af33179 | ||
|
|
3b381b037c | ||
|
|
0968fc56eb | ||
|
|
13044cb5b0 | ||
|
|
580b03f2b9 | ||
|
|
543d4bb6b3 | ||
|
|
21d87d5a80 | ||
|
|
194719e7ba | ||
|
|
dc666fc770 | ||
|
|
50a76e6b27 | ||
|
|
be4d3170db | ||
|
|
7b97002230 | ||
|
|
6319d63e1f | ||
|
|
2e18478b5c |
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"
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
undecided:
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.150",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.152",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -33,6 +38,11 @@ import { Module } from "@nestjs/common";
|
||||
ApiKeysRepository,
|
||||
OAuthClientUsersService,
|
||||
PlatformBookingsService,
|
||||
CalendarsService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
SelectedCalendarsRepository,
|
||||
],
|
||||
controllers: [BookingsController_2024_04_15],
|
||||
})
|
||||
|
||||
@@ -4,19 +4,24 @@ import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bo
|
||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -47,6 +52,11 @@ import { Module } from "@nestjs/common";
|
||||
BookingSeatRepository,
|
||||
ApiKeysRepository,
|
||||
PlatformBookingsService,
|
||||
CalendarsService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
SelectedCalendarsRepository,
|
||||
],
|
||||
controllers: [BookingsController_2024_08_13],
|
||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||
|
||||
@@ -69,9 +69,12 @@ import {
|
||||
@DocsTags("Bookings")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-08-13\``,
|
||||
description: `Must be set to ${VERSION_2024_08_13}`,
|
||||
example: VERSION_2024_08_13,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_08_13,
|
||||
},
|
||||
})
|
||||
export class BookingsController_2024_08_13 {
|
||||
private readonly logger = new Logger("BookingsController_2024_08_13");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
@@ -66,6 +68,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const team2EventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
||||
const phoneOnlyEventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
||||
|
||||
let phoneBasedBooking: BookingOutput_2024_08_13;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
teamUserEmail,
|
||||
@@ -408,12 +412,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
it("should create a phone based booking", async () => {
|
||||
const phoneNumber = "+919876543210";
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||
eventTypeId: phoneOnlyEventTypeId,
|
||||
attendee: {
|
||||
name: "alice",
|
||||
phoneNumber: "+919876543210",
|
||||
phoneNumber,
|
||||
timeZone: "Europe/Madrid",
|
||||
language: "es",
|
||||
},
|
||||
@@ -453,6 +458,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(phoneNumber);
|
||||
phoneBasedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
@@ -619,6 +626,59 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("reschedule", () => {
|
||||
it("should should reschedule phone based booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${phoneBasedBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_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(2035, 0, 8, 14, 15, 0)).toISOString());
|
||||
expect(data.duration).toEqual(15);
|
||||
expect(data.eventTypeId).toEqual(phoneOnlyEventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: phoneBasedBooking.attendees[0].name,
|
||||
email: phoneBasedBooking.attendees[0].email,
|
||||
phoneNumber: phoneBasedBooking.attendees[0].phoneNumber,
|
||||
timeZone: phoneBasedBooking.attendees[0].timeZone,
|
||||
language: phoneBasedBooking.attendees[0].language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(phoneBasedBooking.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(
|
||||
phoneBasedBooking.bookingFieldsResponses.attendeePhoneNumber
|
||||
);
|
||||
phoneBasedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
|
||||
@@ -981,6 +981,64 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("createdAt filter", () => {
|
||||
it("should should get bookings after specified createdAt time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?afterCreatedAt=${createdRecurringBooking[1].createdAt}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings before specified createdAt time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?beforeCreatedAt=${createdRecurringBooking[0].createdAt}`)
|
||||
.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(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings in createdAt range", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/bookings?afterCreatedAt=${createdRecurringBooking[0].createdAt}&beforeCreatedAt=${createdRecurringBooking[1].createdAt}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by start in descending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=desc`)
|
||||
@@ -1891,6 +1949,95 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("rescheduling booking with invalid booking responses", () => {
|
||||
it("should reschedule", async () => {
|
||||
const email = `user-bookings-attendee-${randomString(10)}@gmail.com`;
|
||||
const username = `user-bookings-attendee-${randomString(10)}`;
|
||||
const locale = "it";
|
||||
const timeZone = "Europe/Rome";
|
||||
const attendee = await userRepositoryFixture.create({
|
||||
email,
|
||||
username,
|
||||
locale,
|
||||
timeZone,
|
||||
});
|
||||
|
||||
const booking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-uid-${randomString(10)}`,
|
||||
title: "booking title",
|
||||
startTime: "2060-09-05T11:00:00.000Z",
|
||||
endTime: "2060-09-05T12:00:00.000Z",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
status: "ACCEPTED",
|
||||
metadata: {},
|
||||
responses: "null",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: email,
|
||||
name: username,
|
||||
timeZone,
|
||||
locale,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2060, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${booking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2060, 0, 9, 15, 0, 0)).toISOString());
|
||||
expect(data.rescheduledFromUid).toEqual(booking.uid);
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.hosts[0].email).toEqual(user.email);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.eventTypeId).toEqual(booking.eventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: attendee.username,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.bookingFieldsResponses).toEqual({
|
||||
name: attendee.username,
|
||||
email: attendee.email,
|
||||
rescheduledReason: body.reschedulingReason,
|
||||
guests: [],
|
||||
});
|
||||
await bookingsRepositoryFixture.deleteById(data.id);
|
||||
await userRepositoryFixture.delete(attendee.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
|
||||
return (
|
||||
!Array.isArray(data) &&
|
||||
|
||||
@@ -229,8 +229,7 @@ export class BookingsService_2024_08_13 {
|
||||
const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({
|
||||
bookingListingByStatus: queryParams.status || [],
|
||||
skip: queryParams.skip ?? 0,
|
||||
// note(Lauris): we substract -1 because getAllUSerBookings child function adds +1 for some reason
|
||||
take: queryParams.take ? queryParams.take - 1 : 100,
|
||||
take: queryParams.take ?? 100,
|
||||
filters: this.inputService.transformGetBookingsFilters(queryParams),
|
||||
ctx: {
|
||||
user,
|
||||
@@ -564,7 +563,9 @@ export class BookingsService_2024_08_13 {
|
||||
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
|
||||
}
|
||||
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdIncludeUsersAndTeam(booking.eventTypeId);
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdIncludeUsersAndTeam(
|
||||
booking.eventTypeId
|
||||
);
|
||||
if (!eventType) {
|
||||
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { defaultBookingResponses } from "@/lib/safe-parse/default-responses-booking";
|
||||
import { safeParse } from "@/lib/safe-parse/safe-parse";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
@@ -41,7 +43,7 @@ import {
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { BookingInputLocation_2024_08_13 } from "@calcom/platform-types/bookings/2024-08-13/inputs/location.input";
|
||||
import { EventType, PlatformOAuthClient } from "@calcom/prisma/client";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
|
||||
|
||||
@@ -429,6 +431,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
...(inputBooking.bookingFieldsResponses || {}),
|
||||
name: inputBooking.attendee.name,
|
||||
email: attendeeEmail,
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
guests,
|
||||
location,
|
||||
},
|
||||
@@ -567,8 +570,19 @@ export class InputBookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
|
||||
}
|
||||
|
||||
const bookingResponses = bookingResponsesSchema.parse(booking.responses);
|
||||
const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
|
||||
const bookingResponses = safeParse(
|
||||
bookingResponsesSchema,
|
||||
booking.responses,
|
||||
defaultBookingResponses,
|
||||
false
|
||||
);
|
||||
const bookingResponsesMissing =
|
||||
bookingResponses.name === defaultBookingResponses.name &&
|
||||
bookingResponses.email === defaultBookingResponses.email;
|
||||
|
||||
const attendee = bookingResponsesMissing
|
||||
? booking.attendees[0]
|
||||
: booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
|
||||
|
||||
if (!attendee) {
|
||||
throw new NotFoundException(
|
||||
@@ -576,6 +590,12 @@ export class InputBookingsService_2024_08_13 {
|
||||
);
|
||||
}
|
||||
|
||||
if (bookingResponsesMissing) {
|
||||
bookingResponses.name = attendee.name;
|
||||
bookingResponses.email = attendee.email;
|
||||
bookingResponses.attendeePhoneNumber = attendee.phoneNumber || undefined;
|
||||
}
|
||||
|
||||
const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone);
|
||||
const endTime = startTime.plus({ minutes: eventType.length });
|
||||
|
||||
@@ -632,6 +652,8 @@ export class InputBookingsService_2024_08_13 {
|
||||
queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined),
|
||||
afterUpdatedDate: queryParams.afterUpdatedAt,
|
||||
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
||||
afterCreatedDate: queryParams.afterCreatedAt,
|
||||
beforeCreatedDate: queryParams.beforeCreatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Booking, BookingSeat } from "@calcom/prisma/client";
|
||||
export const bookingResponsesSchema = z
|
||||
.object({
|
||||
email: z.string(),
|
||||
attendeePhoneNumber: z.string().optional(),
|
||||
name: z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
@@ -45,6 +46,7 @@ export const seatedBookingDataSchema = z
|
||||
responses: z
|
||||
.object({
|
||||
email: z.string(),
|
||||
attendeePhoneNumber: z.string().optional(),
|
||||
name: z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
|
||||
+50
-1
@@ -116,6 +116,7 @@ describe("Event types Endpoints", () => {
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldLocation = {
|
||||
@@ -336,6 +337,46 @@ describe("Event types Endpoints", () => {
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("should not be able to create phone-only event type", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Phone coding consultation",
|
||||
slug: "phone-coding-consultation",
|
||||
description: "Our team will review your codebase.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
],
|
||||
bookingFields: [
|
||||
{
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message).toBe(
|
||||
"checkIsEmailUserAccessible - Email booking field must be required and visible"
|
||||
);
|
||||
});
|
||||
|
||||
it("should create an event type", async () => {
|
||||
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
|
||||
type: "name",
|
||||
@@ -1191,7 +1232,14 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
const expectedReturnSystemFields = [
|
||||
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{
|
||||
isDefault: true,
|
||||
required: true,
|
||||
slug: "email",
|
||||
type: "email",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
@@ -1644,6 +1692,7 @@ describe("Event types Endpoints", () => {
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
|
||||
+11
-2
@@ -29,7 +29,12 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
EVENT_TYPE_READ,
|
||||
EVENT_TYPE_WRITE,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_06_14,
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
GetEventTypesQuery_2024_06_14,
|
||||
@@ -45,8 +50,12 @@ import {
|
||||
@DocsTags("Event Types")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-06-14\``,
|
||||
description: `Must be set to ${VERSION_2024_06_14}`,
|
||||
example: VERSION_2024_06_14,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_06_14,
|
||||
},
|
||||
})
|
||||
export class EventTypesController_2024_06_14 {
|
||||
constructor(
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
updateEventType,
|
||||
getEventTypesPublic,
|
||||
EventTypesPublic,
|
||||
SystemField,
|
||||
CustomField,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
@@ -31,6 +33,9 @@ export class EventTypesService_2024_06_14 {
|
||||
) {}
|
||||
|
||||
async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) {
|
||||
if (body.bookingFields) {
|
||||
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
|
||||
}
|
||||
await this.checkCanCreateEventType(user.id, body);
|
||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||
const { destinationCalendar: _destinationCalendar, ...rest } = body;
|
||||
@@ -78,6 +83,16 @@ export class EventTypesService_2024_06_14 {
|
||||
await this.checkUserOwnsSchedule(userId, body.scheduleId);
|
||||
}
|
||||
|
||||
checkHasUserAccessibleEmailBookingField(bookingFields: (SystemField | CustomField)[]) {
|
||||
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
|
||||
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
|
||||
if (!isEmailFieldRequiredAndVisible) {
|
||||
throw new BadRequestException(
|
||||
"checkIsEmailUserAccessible - Email booking field must be required and visible"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getEventTypeByUsernameAndSlug(
|
||||
username: string,
|
||||
eventTypeSlug: string,
|
||||
@@ -219,6 +234,9 @@ export class EventTypesService_2024_06_14 {
|
||||
body: Partial<InputEventTransformed_2024_06_14>,
|
||||
user: UserWithProfile
|
||||
) {
|
||||
if (body.bookingFields) {
|
||||
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
|
||||
}
|
||||
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
|
||||
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||
|
||||
|
||||
+25
-14
@@ -37,8 +37,6 @@ import {
|
||||
} from "@calcom/platform-types";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
|
||||
|
||||
interface ValidationContext {
|
||||
eventTypeId?: number;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
@@ -51,7 +49,6 @@ interface ValidationContext {
|
||||
export class InputEventTypesService_2024_06_14 {
|
||||
constructor(
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
|
||||
private readonly calendarsService: CalendarsService
|
||||
) {}
|
||||
|
||||
@@ -128,12 +125,11 @@ export class InputEventTypesService_2024_06_14 {
|
||||
} = inputEventType;
|
||||
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
||||
|
||||
const hasMultipleLocations = (locations || defaultLocations).length > 1;
|
||||
const eventType = {
|
||||
...rest,
|
||||
length: lengthInMinutes,
|
||||
locations: this.transformInputLocations(locations || defaultLocations),
|
||||
bookingFields: this.transformInputBookingFields(bookingFields, hasMultipleLocations),
|
||||
bookingFields: this.transformInputBookingFields(bookingFields),
|
||||
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||
durationLimits: bookingLimitsDuration
|
||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||
@@ -182,15 +178,12 @@ export class InputEventTypesService_2024_06_14 {
|
||||
: {};
|
||||
|
||||
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
||||
const hasMultipleLocations = !!(locations && locations?.length > 1);
|
||||
|
||||
const eventType = {
|
||||
...rest,
|
||||
length: lengthInMinutes,
|
||||
locations: locations ? this.transformInputLocations(locations) : undefined,
|
||||
bookingFields: bookingFields
|
||||
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
|
||||
: undefined,
|
||||
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
|
||||
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||
durationLimits: bookingLimitsDuration
|
||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||
@@ -220,10 +213,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
return transformLocationsApiToInternal(inputLocations);
|
||||
}
|
||||
|
||||
transformInputBookingFields(
|
||||
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
|
||||
hasMultipleLocations: boolean
|
||||
) {
|
||||
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
|
||||
const internalFields: (SystemField | CustomField)[] = inputBookingFields
|
||||
? transformBookingFieldsApiToInternal(inputBookingFields)
|
||||
: [];
|
||||
@@ -252,7 +242,28 @@ export class InputEventTypesService_2024_06_14 {
|
||||
systemCustomRescheduleReasonField || systemAfterFieldRescheduleReason,
|
||||
];
|
||||
|
||||
return [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
|
||||
const bookingFields = [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
|
||||
|
||||
if (!this.hasEmailOrPhoneOnlySetup(bookingFields)) {
|
||||
throw new BadRequestException(
|
||||
"Booking fields validation failed: visible and required email or visible and required attendee phone field is needed."
|
||||
);
|
||||
}
|
||||
|
||||
return bookingFields;
|
||||
}
|
||||
|
||||
hasEmailOrPhoneOnlySetup(bookingFields: (SystemField | CustomField)[]) {
|
||||
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
|
||||
const attendeePhoneNumberField = bookingFields.find(
|
||||
(field) => field.type === "phone" && field.name === "attendeePhoneNumber"
|
||||
);
|
||||
|
||||
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
|
||||
const isAttendeePhoneNumberFieldRequiredAndVisible =
|
||||
attendeePhoneNumberField?.required && !attendeePhoneNumberField?.hidden;
|
||||
|
||||
return isEmailFieldRequiredAndVisible || isAttendeePhoneNumberFieldRequiredAndVisible;
|
||||
}
|
||||
|
||||
isUserCustomField(field: SystemField | CustomField): field is CustomField {
|
||||
|
||||
+5
-1
@@ -39,8 +39,12 @@ import {
|
||||
@DocsTags("Schedules")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-06-11\``,
|
||||
description: `Must be set to ${VERSION_2024_06_11}`,
|
||||
example: VERSION_2024_06_11,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_06_11,
|
||||
},
|
||||
})
|
||||
@ApiHeader({
|
||||
name: "Authorization",
|
||||
|
||||
@@ -2,7 +2,7 @@ export const defaultBookingResponses = {
|
||||
name: "unknown",
|
||||
email: "unknown",
|
||||
guests: [],
|
||||
rescheduleReason: "unknown",
|
||||
rescheduledReason: "unknown",
|
||||
};
|
||||
|
||||
export const defaultBookingMetadata = { videoCallUrl: "unknown" };
|
||||
|
||||
@@ -21,13 +21,23 @@ export class ResponseInterceptor implements NestInterceptor {
|
||||
const { statusCode } = response;
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
let jsonBodyString = "{}";
|
||||
|
||||
try {
|
||||
if (data && typeof data === "object") {
|
||||
jsonBodyString = JSON.stringify(data);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error("Could not parse request body");
|
||||
}
|
||||
|
||||
this.logger.log("Outgoing Response", {
|
||||
requestId,
|
||||
method,
|
||||
url,
|
||||
statusCode,
|
||||
responseTime,
|
||||
responseBody: data,
|
||||
responseBody: jsonBodyString,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
})
|
||||
|
||||
@@ -11,13 +11,22 @@ export class RequestIdMiddleware implements NestMiddleware {
|
||||
const requestId = uuid();
|
||||
req.headers["X-Request-Id"] = requestId;
|
||||
const { method, headers, body: requestBody, baseUrl } = req;
|
||||
let jsonBodyString = "{}";
|
||||
|
||||
try {
|
||||
if (requestBody && typeof requestBody === "object") {
|
||||
jsonBodyString = JSON.stringify(requestBody);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error("Could not parse request body");
|
||||
}
|
||||
|
||||
this.logger.log("Incoming Request", {
|
||||
requestId,
|
||||
method,
|
||||
url: baseUrl,
|
||||
headers: filterReqHeaders(headers),
|
||||
requestBody,
|
||||
requestBody: jsonBodyString,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
|
||||
@@ -35,12 +35,13 @@ import {
|
||||
Redirect,
|
||||
UnauthorizedException,
|
||||
Req,
|
||||
HttpException,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { Request } from "express";
|
||||
|
||||
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS, OFFICE_365_VIDEO } from "@calcom/platform-constants";
|
||||
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS, OFFICE_365_VIDEO, CAL_VIDEO } from "@calcom/platform-constants";
|
||||
|
||||
export type OAuthCallbackState = {
|
||||
accessToken: string;
|
||||
@@ -56,7 +57,7 @@ export type OAuthCallbackState = {
|
||||
})
|
||||
@DocsTags("Conferencing")
|
||||
export class ConferencingController {
|
||||
private readonly logger = new Logger("Platform Gcal Provider");
|
||||
private readonly logger = new Logger("ConferencingController");
|
||||
|
||||
constructor(
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
@@ -70,6 +71,12 @@ export class ConferencingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Connect your conferencing application" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [GOOGLE_MEET],
|
||||
required: true,
|
||||
})
|
||||
async connect(
|
||||
@GetUser("id") userId: number,
|
||||
@Param("app") app: string
|
||||
@@ -92,6 +99,12 @@ export class ConferencingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Get OAuth conferencing app auth url" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [ZOOM, OFFICE_365_VIDEO],
|
||||
required: true,
|
||||
})
|
||||
async redirect(
|
||||
@Req() req: Request,
|
||||
@Headers("Authorization") authorization: string,
|
||||
@@ -137,6 +150,12 @@ export class ConferencingController {
|
||||
@UseGuards()
|
||||
@Redirect(undefined, 301)
|
||||
@ApiOperation({ summary: "conferencing apps oauths callback" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [ZOOM, OFFICE_365_VIDEO],
|
||||
required: true,
|
||||
})
|
||||
async save(
|
||||
@Query("state") state: string,
|
||||
@Param("app") app: string,
|
||||
@@ -144,6 +163,10 @@ export class ConferencingController {
|
||||
@Query("error") error: string | undefined,
|
||||
@Query("error_description") error_description: string | undefined
|
||||
): Promise<{ url: string }> {
|
||||
if (!state) {
|
||||
throw new BadRequestException("Missing `state` query param");
|
||||
}
|
||||
|
||||
const decodedCallbackState: OAuthCallbackState = JSON.parse(state);
|
||||
try {
|
||||
const userId = await this.tokensRepository.getAccessTokenOwnerId(decodedCallbackState.accessToken);
|
||||
@@ -169,6 +192,9 @@ export class ConferencingController {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException || error instanceof Error) {
|
||||
this.logger.error(error.message);
|
||||
}
|
||||
return {
|
||||
url: decodedCallbackState.onErrorReturnTo ?? "",
|
||||
};
|
||||
@@ -195,6 +221,12 @@ export class ConferencingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Set your default conferencing application" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO, CAL_VIDEO],
|
||||
required: true,
|
||||
})
|
||||
async default(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Param("app") app: string
|
||||
@@ -216,6 +248,12 @@ export class ConferencingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Disconnect your conferencing application" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO],
|
||||
required: true,
|
||||
})
|
||||
async disconnect(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Param("app") app: string
|
||||
|
||||
+7
@@ -26,6 +26,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { slugify } from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:4321";
|
||||
@@ -228,6 +229,9 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat);
|
||||
expect(responseBody.data.user.locale).toEqual(requestBody.locale);
|
||||
expect(responseBody.data.user.avatarUrl).toEqual(requestBody.avatarUrl);
|
||||
const [emailUser, emailDomain] = responseBody.data.user.email.split("@");
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
expect(responseBody.data.user.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`));
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
@@ -546,6 +550,9 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.data.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, userUpdatedEmail)
|
||||
);
|
||||
const [emailUser, emailDomain] = responseBody.data.email.split("@");
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
expect(responseBody.data.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`));
|
||||
expect(responseBody.data.locale).toEqual(Locales.PT_BR);
|
||||
});
|
||||
|
||||
|
||||
+1
-14
@@ -29,19 +29,12 @@ import {
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiOperation as DocsOperation,
|
||||
ApiCreatedResponse as DocsCreatedResponse,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { ApiCreatedResponse as DocsCreatedResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { User, MembershipRole } from "@prisma/client";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { CreateOAuthClientInput, UpdateOAuthClientInput, Pagination } from "@calcom/platform-types";
|
||||
|
||||
const AUTH_DOCUMENTATION = `⚠️ First, this endpoint requires \`Cookie: next-auth.session-token=eyJhbGciOiJ\` header. Log into Cal web app using owner of organization that was created after visiting \`/settings/organizations/new\`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.
|
||||
Second, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.`;
|
||||
|
||||
@Controller({
|
||||
path: "/v2/oauth-clients",
|
||||
version: API_VERSIONS_VALUES,
|
||||
@@ -61,7 +54,6 @@ export class OAuthClientsController {
|
||||
@Post("/")
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@DocsCreatedResponse({
|
||||
description: "Create an OAuth client",
|
||||
type: CreateOAuthClientResponseDto,
|
||||
@@ -90,7 +82,6 @@ export class OAuthClientsController {
|
||||
@Get("/")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
async getOAuthClients(@GetOrgId() organizationId: number): Promise<GetOAuthClientsResponseDto> {
|
||||
const clients = await this.oAuthClientsService.getOAuthClients(organizationId);
|
||||
return { status: SUCCESS_STATUS, data: clients };
|
||||
@@ -99,7 +90,6 @@ export class OAuthClientsController {
|
||||
@Get("/:clientId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async getOAuthClientById(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
|
||||
const client = await this.oAuthClientsService.getOAuthClientById(clientId);
|
||||
@@ -109,7 +99,6 @@ export class OAuthClientsController {
|
||||
@Get("/:clientId/managed-users")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async getOAuthClientManagedUsersById(
|
||||
@Param("clientId") clientId: string,
|
||||
@@ -128,7 +117,6 @@ export class OAuthClientsController {
|
||||
@Patch("/:clientId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async updateOAuthClient(
|
||||
@Param("clientId") clientId: string,
|
||||
@@ -142,7 +130,6 @@ export class OAuthClientsController {
|
||||
@Delete("/:clientId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async deleteOAuthClient(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
|
||||
this.logger.log(`Deleting OAuth Client with ID: ${clientId}`);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { AuthModule } from "@/modules/auth/auth.module";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OAuthClientUsersController } from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller";
|
||||
import { OAuthClientsController } from "@/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller";
|
||||
@@ -16,6 +20,7 @@ import { OrganizationsModule } from "@/modules/organizations/organizations.modul
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
@@ -42,6 +47,11 @@ import { JwtService } from "@nestjs/jwt";
|
||||
OAuthClientRepository,
|
||||
TokensRepository,
|
||||
OAuthFlowService,
|
||||
CalendarsService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
SelectedCalendarsRepository,
|
||||
OAuthClientUsersService,
|
||||
OrganizationsTeamsService,
|
||||
OAuthClientsService,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { GetManagedUsersInput } from "@/modules/oauth-clients/controllers/oauth-client-users/inputs/get-managed-users.input";
|
||||
@@ -5,18 +6,21 @@ import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
|
||||
import { BadRequestException, ConflictException, Injectable, Logger } from "@nestjs/common";
|
||||
import { User, CreationSource, PlatformOAuthClient } from "@prisma/client";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientUsersService {
|
||||
private readonly logger = new Logger("OAuthClientUsersService");
|
||||
|
||||
constructor(
|
||||
private readonly userRepository: UsersRepository,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly eventTypesService: EventTypesService_2024_04_15,
|
||||
private readonly schedulesService: SchedulesService_2024_04_15
|
||||
private readonly schedulesService: SchedulesService_2024_04_15,
|
||||
private readonly calendarsService: CalendarsService
|
||||
) {}
|
||||
|
||||
async createOAuthClientUser(oAuthClient: PlatformOAuthClient, body: CreateManagedUserInput) {
|
||||
@@ -87,6 +91,13 @@ export class OAuthClientUsersService {
|
||||
user.defaultScheduleId = defaultSchedule.id;
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger.log(`Setting default calendars in db for user with id ${user.id}`);
|
||||
await this.calendarsService.getCalendars(user.id);
|
||||
} catch (err) {
|
||||
this.logger.error(`Could not get calendars of new managed user with id ${user.id}`);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
tokens: {
|
||||
@@ -123,7 +134,9 @@ export class OAuthClientUsersService {
|
||||
if (body.email) {
|
||||
const emailWithOAuthId = OAuthClientUsersService.getOAuthUserEmail(oAuthClientId, body.email);
|
||||
body.email = emailWithOAuthId;
|
||||
const newUsername = slugify(emailWithOAuthId);
|
||||
const [emailUser, emailDomain] = emailWithOAuthId.split("@");
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
const newUsername = slugify(`${emailUser}-${domainName}-${TLD}`);
|
||||
await this.userRepository.updateUsername(userId, newUsername);
|
||||
}
|
||||
|
||||
|
||||
+10
-8
@@ -38,6 +38,15 @@ export class OrganizationsDelegationCredentialService {
|
||||
delegatedServiceAccountUser: User,
|
||||
body: UpdateDelegationCredentialInput
|
||||
) {
|
||||
const delegationCredential =
|
||||
await this.organizationsDelegationCredentialRepository.findByIdWithWorkspacePlatform(
|
||||
delegationCredentialId
|
||||
);
|
||||
|
||||
if (!delegationCredential) {
|
||||
throw new NotFoundException(`DelegationCredential with id ${delegationCredentialId} not found`);
|
||||
}
|
||||
|
||||
if (body.enabled !== undefined) {
|
||||
await this.updateDelegationCredentialEnabled(
|
||||
orgId,
|
||||
@@ -49,15 +58,8 @@ export class OrganizationsDelegationCredentialService {
|
||||
if (body.serviceAccountKey !== undefined) {
|
||||
await this.updateDelegationCredentialServiceAccountKey(delegationCredentialId, body.serviceAccountKey);
|
||||
}
|
||||
const delegationCredential =
|
||||
await this.organizationsDelegationCredentialRepository.findByIdWithWorkspacePlatform(
|
||||
delegationCredentialId
|
||||
);
|
||||
if (!delegationCredential) {
|
||||
throw new NotFoundException(`DelegationCredential with id ${delegationCredentialId} not found`);
|
||||
}
|
||||
|
||||
return delegationCredential;
|
||||
return { ...delegationCredential, enabled: body?.enabled ?? delegationCredential?.enabled };
|
||||
}
|
||||
|
||||
async updateDelegationCredentialEnabled(
|
||||
|
||||
+2
-2
@@ -81,7 +81,7 @@ export class OrganizationsEventTypesController {
|
||||
bodyEventType
|
||||
);
|
||||
|
||||
const eventType = await this.organizationsEventTypesService.createTeamEventType(
|
||||
const eventType = await this.organizationsEventTypesService.createOrganizationTeamEventType(
|
||||
user,
|
||||
teamId,
|
||||
orgId,
|
||||
@@ -212,7 +212,7 @@ export class OrganizationsEventTypesController {
|
||||
bodyEventType
|
||||
);
|
||||
|
||||
const eventType = await this.organizationsEventTypesService.updateTeamEventType(
|
||||
const eventType = await this.organizationsEventTypesService.updateOrganizationTeamEventType(
|
||||
eventTypeId,
|
||||
teamId,
|
||||
transformedBody,
|
||||
|
||||
+129
-3
@@ -581,6 +581,123 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to create phone-only event type", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: "Phone coding consultation",
|
||||
slug: "phone-coding-consultation",
|
||||
description: "Our team will review your codebase.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "organizersDefaultApp",
|
||||
},
|
||||
],
|
||||
schedulingType: "COLLECTIVE",
|
||||
hosts: [
|
||||
{
|
||||
userId: teammate1.id,
|
||||
mandatory: true,
|
||||
priority: "high",
|
||||
},
|
||||
],
|
||||
bookingFields: [
|
||||
{
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data = responseBody.data;
|
||||
expect(data.bookingFields).toEqual([
|
||||
{
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
hidden: false,
|
||||
label: "Phone number",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to configure phone-only event type", async () => {
|
||||
const body: UpdateTeamEventTypeInput_2024_06_14 = {
|
||||
bookingFields: [
|
||||
@@ -588,13 +705,14 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: true,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -622,6 +740,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
required: false,
|
||||
label: "Email",
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
@@ -635,7 +754,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
hidden: true,
|
||||
hidden: false,
|
||||
label: "Phone number",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
@@ -776,7 +895,14 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{
|
||||
isDefault: true,
|
||||
required: true,
|
||||
slug: "email",
|
||||
type: "email",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
disableOnPrefill: false,
|
||||
isDefault: true,
|
||||
|
||||
+4
-4
@@ -26,7 +26,7 @@ export class OrganizationsEventTypesService {
|
||||
private readonly usersService: UsersService
|
||||
) {}
|
||||
|
||||
async createTeamEventType(
|
||||
async createOrganizationTeamEventType(
|
||||
user: UserWithProfile,
|
||||
teamId: number,
|
||||
orgId: number,
|
||||
@@ -48,7 +48,7 @@ export class OrganizationsEventTypesService {
|
||||
},
|
||||
});
|
||||
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user, true);
|
||||
}
|
||||
|
||||
async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) {
|
||||
@@ -91,13 +91,13 @@ export class OrganizationsEventTypesService {
|
||||
return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(orgId, skip, take);
|
||||
}
|
||||
|
||||
async updateTeamEventType(
|
||||
async updateOrganizationTeamEventType(
|
||||
eventTypeId: number,
|
||||
teamId: number,
|
||||
body: TransformedUpdateTeamEventTypeInput,
|
||||
user: UserWithProfile
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user);
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user, true);
|
||||
}
|
||||
|
||||
async deleteTeamEventType(teamId: number, eventTypeId: number) {
|
||||
|
||||
+4
-3
@@ -37,11 +37,12 @@ export class OrganizationsMembershipRepository {
|
||||
}
|
||||
|
||||
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
|
||||
return this.dbWrite.prisma.membership.create({
|
||||
data: { ...data, teamId: organizationId },
|
||||
return this.dbWrite.prisma.membership.upsert({
|
||||
create: { ...data, teamId: organizationId },
|
||||
update: { role: data.role, accepted: data.accepted, disableImpersonation: data.disableImpersonation },
|
||||
where: { userId_teamId: { userId: data.userId, teamId: organizationId } },
|
||||
});
|
||||
}
|
||||
|
||||
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
||||
return this.dbWrite.prisma.membership.update({
|
||||
data: { ...data },
|
||||
|
||||
+98
@@ -75,6 +75,9 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
const seatedEventTypeSlug = `slots-2024-09-04-seated-event-type-${randomString()}`;
|
||||
let seatedEventType: EventType;
|
||||
|
||||
const variableLengthEventTypeSlug = `slots-2024-09-04-variable-length-event-type-${randomString()}`;
|
||||
let variableLengthEventType: EventType;
|
||||
|
||||
let reservedSlot: ReserveSlotOutputData_2024_09_04;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -168,6 +171,17 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
);
|
||||
seatedEventType = seatedEvent;
|
||||
|
||||
const variableLengthEvent = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: "frisbee match",
|
||||
slug: `slots-2024-09-04-variable-length-event-type-${randomString()}`,
|
||||
length: 15,
|
||||
metadata: { multipleDuration: [15, 30, 45, 60] },
|
||||
},
|
||||
user.id
|
||||
);
|
||||
variableLengthEventType = variableLengthEvent;
|
||||
|
||||
team = await teamRepositoryFixture.create({
|
||||
name: `slots-2024-09-04-team-${randomString()}`,
|
||||
isOrganization: false,
|
||||
@@ -1246,6 +1260,90 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
await bookingsRepositoryFixture.deleteById(booking.id);
|
||||
});
|
||||
|
||||
describe("variable length", () => {
|
||||
it("should not be able to reserve a slot for variable length event type with invalid duration", async () => {
|
||||
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
||||
const reserveResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/slots/reservations`)
|
||||
.send({
|
||||
eventTypeId: variableLengthEventType.id,
|
||||
slotStart: slotStartTime,
|
||||
slotDuration: 1000,
|
||||
})
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(400);
|
||||
|
||||
expect(reserveResponse.body.error.message).toEqual(
|
||||
"Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: 15, 30, 45, 60"
|
||||
);
|
||||
});
|
||||
|
||||
it("should reserve a slot with slot duration for variable event type length", async () => {
|
||||
// note(Lauris): mock current date to test slots release time
|
||||
const now = "2049-09-05T12:00:00.000Z";
|
||||
const newDate = DateTime.fromISO(now, { zone: "UTC" }).toJSDate();
|
||||
advanceTo(newDate);
|
||||
|
||||
const slotDuration = 60;
|
||||
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
||||
const reserveResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/slots/reservations`)
|
||||
.send({
|
||||
eventTypeId: variableLengthEventType.id,
|
||||
slotStart: slotStartTime,
|
||||
slotDuration,
|
||||
})
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(201);
|
||||
|
||||
const reserveResponseBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponse.body;
|
||||
expect(reserveResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const responseReservedSlot: ReserveSlotOutputData_2024_09_04 = reserveResponseBody.data;
|
||||
expect(responseReservedSlot.reservationUid).toBeDefined();
|
||||
expect(responseReservedSlot.eventTypeId).toEqual(variableLengthEventType.id);
|
||||
expect(responseReservedSlot.slotStart).toEqual(slotStartTime);
|
||||
expect(responseReservedSlot.slotDuration).toEqual(slotDuration);
|
||||
expect(responseReservedSlot.slotEnd).toEqual(
|
||||
DateTime.fromISO(slotStartTime, { zone: "UTC" }).plus({ minutes: slotDuration }).toISO()
|
||||
);
|
||||
expect(responseReservedSlot.reservationDuration).toEqual(5);
|
||||
|
||||
if (!responseReservedSlot.reservationUid) {
|
||||
throw new Error("Reserved slot uid is undefined");
|
||||
}
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/slots?eventTypeId=${variableLengthEventType.id}&start=2050-09-05&end=2050-09-09&duration=60`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetSlotsOutput_2024_09_04 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const slots = responseBody.data;
|
||||
|
||||
expect(slots).toBeDefined();
|
||||
const days = Object.keys(slots);
|
||||
expect(days.length).toEqual(5);
|
||||
|
||||
const expectedSlotsUTC2050_09_05 = expectedSlotsUTC["2050-09-05"].filter(
|
||||
(slot) => slot.start !== slotStartTime
|
||||
);
|
||||
expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 });
|
||||
|
||||
const dbSlot = await selectedSlotsRepositoryFixture.getByUid(reservedSlot.reservationUid);
|
||||
expect(dbSlot).toBeDefined();
|
||||
if (dbSlot) {
|
||||
const dbReleaseAt = DateTime.fromJSDate(dbSlot.releaseAt, { zone: "UTC" }).toISO();
|
||||
const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 5 }).toISO();
|
||||
expect(dbReleaseAt).toEqual(expectedReleaseAt);
|
||||
expect(responseReservedSlot.reservationUntil).toEqual(expectedReleaseAt);
|
||||
}
|
||||
clear();
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(unrelatedUser.email);
|
||||
|
||||
@@ -47,8 +47,12 @@ import { ApiResponse } from "@calcom/platform-types";
|
||||
@DocsTags("Slots")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-09-04\``,
|
||||
description: `Must be set to ${VERSION_2024_09_04}`,
|
||||
example: VERSION_2024_09_04,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_09_04,
|
||||
},
|
||||
})
|
||||
export class SlotsController_2024_09_04 {
|
||||
constructor(private readonly slotsService: SlotsService_2024_09_04) {}
|
||||
|
||||
@@ -21,9 +21,11 @@ import { getAvailableSlots } from "@calcom/platform-libraries/slots";
|
||||
import { GetSlotsInput_2024_09_04, ReserveSlotInput_2024_09_04 } from "@calcom/platform-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
const eventTypeMetadataSchema = z.object({
|
||||
multipleDuration: z.number().array().optional(),
|
||||
});
|
||||
const eventTypeMetadataSchema = z
|
||||
.object({
|
||||
multipleDuration: z.number().array().optional(),
|
||||
})
|
||||
.nullable();
|
||||
|
||||
const DEFAULT_RESERVATION_DURATION = 5;
|
||||
|
||||
@@ -89,17 +91,8 @@ export class SlotsService_2024_09_04 {
|
||||
throw new BadRequestException("Invalid start date");
|
||||
}
|
||||
|
||||
const metadata = eventTypeMetadataSchema.parse(eventType);
|
||||
if (
|
||||
input.slotDuration &&
|
||||
metadata.multipleDuration &&
|
||||
!metadata.multipleDuration.includes(input.slotDuration)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: ${metadata.multipleDuration.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
if (input.slotDuration) {
|
||||
this.validateSlotDuration(eventType, input.slotDuration);
|
||||
}
|
||||
|
||||
const endDate = startDate.plus({ minutes: input.slotDuration ?? eventType.length });
|
||||
@@ -160,6 +153,23 @@ export class SlotsService_2024_09_04 {
|
||||
return this.slotsOutputService.getReservationSlotCreated(slot, reservationDuration);
|
||||
}
|
||||
|
||||
validateSlotDuration(eventType: EventType, inputSlotDuration: number) {
|
||||
const eventTypeMetadata = eventTypeMetadataSchema.parse(eventType.metadata);
|
||||
if (!eventTypeMetadata?.multipleDuration) {
|
||||
throw new BadRequestException(
|
||||
"You passed 'slotDuration' but this event type is not a variable length event type."
|
||||
);
|
||||
}
|
||||
|
||||
if (!eventTypeMetadata.multipleDuration.includes(inputSlotDuration)) {
|
||||
throw new BadRequestException(
|
||||
`Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: ${eventTypeMetadata.multipleDuration.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async canSpecifyCustomReservationDuration(authUserId: number, eventType: EventType) {
|
||||
if (eventType.userId) {
|
||||
return await this.canSpecifyCustomReservationDurationIndividualEvent(authUserId, eventType.userId);
|
||||
@@ -214,17 +224,8 @@ export class SlotsService_2024_09_04 {
|
||||
throw new BadRequestException("Invalid start date");
|
||||
}
|
||||
|
||||
const metadata = eventTypeMetadataSchema.parse(eventType);
|
||||
if (
|
||||
input.slotDuration &&
|
||||
metadata.multipleDuration &&
|
||||
!metadata.multipleDuration.includes(input.slotDuration)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: ${metadata.multipleDuration.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
if (input.slotDuration) {
|
||||
this.validateSlotDuration(eventType, input.slotDuration);
|
||||
}
|
||||
|
||||
const endDate = startDate.plus({ minutes: input.slotDuration ?? eventType.length });
|
||||
|
||||
+50
@@ -167,6 +167,56 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
return request(app.getHttpServer()).post(`/v2/teams/${team.id}/event-types`).send(body).expect(404);
|
||||
});
|
||||
|
||||
it("should not be able to create phone-only event type", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: "Phone coding consultation",
|
||||
slug: "phone-coding-consultation",
|
||||
description: "Our team will review your codebase.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "organizersDefaultApp",
|
||||
},
|
||||
],
|
||||
schedulingType: "COLLECTIVE",
|
||||
hosts: [
|
||||
{
|
||||
userId: teamMember1.id,
|
||||
},
|
||||
{
|
||||
userId: teamMember2.id,
|
||||
},
|
||||
],
|
||||
bookingFields: [
|
||||
{
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/teams/${team.id}/event-types`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toBe(
|
||||
"checkIsEmailUserAccessible - Email booking field must be required and visible"
|
||||
);
|
||||
});
|
||||
|
||||
it("should create a collective team event-type", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: `teams-event-types-collective-${randomString()}`,
|
||||
|
||||
+2
-1
@@ -181,7 +181,8 @@ export class TeamsEventTypesController {
|
||||
eventTypeId,
|
||||
teamId,
|
||||
transformedBody,
|
||||
user
|
||||
user,
|
||||
false
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -30,6 +30,10 @@ export class TeamsEventTypesService {
|
||||
teamId: number,
|
||||
body: TransformedCreateTeamEventTypeInput
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
// note(Lauris): once phone only event types / bookings are enabled for simple users remove checkHasUserAccessibleEmailBookingField check
|
||||
if (body.bookingFields) {
|
||||
this.eventTypesService.checkHasUserAccessibleEmailBookingField(body.bookingFields);
|
||||
}
|
||||
const eventTypeUser = await this.getUserToCreateTeamEvent(user);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { hosts, children, destinationCalendar, ...rest } = body;
|
||||
@@ -44,7 +48,7 @@ export class TeamsEventTypesService {
|
||||
},
|
||||
});
|
||||
|
||||
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
|
||||
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user, false);
|
||||
}
|
||||
|
||||
async validateEventTypeExists(teamId: number, eventTypeId: number) {
|
||||
@@ -105,8 +109,14 @@ export class TeamsEventTypesService {
|
||||
eventTypeId: number,
|
||||
teamId: number,
|
||||
body: TransformedUpdateTeamEventTypeInput,
|
||||
user: UserWithProfile
|
||||
user: UserWithProfile,
|
||||
// note(Lauris): once phone only event types / bookings are enabled for simple users remove isOrg parameter (right now only organization team event types support hidden / non-required email field)
|
||||
isOrg: boolean
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
if (!isOrg && body.bookingFields) {
|
||||
// note(Lauris): once phone only event types / bookings are enabled for simple users remove checkHasUserAccessibleEmailBookingField check
|
||||
this.eventTypesService.checkHasUserAccessibleEmailBookingField(body.bookingFields);
|
||||
}
|
||||
await this.validateEventTypeExists(teamId, eventTypeId);
|
||||
const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user);
|
||||
await updateEventType({
|
||||
|
||||
@@ -3597,10 +3597,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"description": "Must be set to 2024-08-13",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-08-13"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -3648,10 +3649,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"description": "Must be set to 2024-08-13",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-08-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -3754,6 +3756,26 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "afterCreatedAt",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter bookings that have been created after this date string.",
|
||||
"example": "?afterCreatedAt=2025-03-07T10:00:00.000Z",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "beforeCreatedAt",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter bookings that have been created before this date string.",
|
||||
"example": "?beforeCreatedAt=2025-03-14T11:00:00.000Z",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "afterUpdatedAt",
|
||||
"required": false,
|
||||
@@ -3886,10 +3908,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"description": "Must be set to 2024-08-13",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-08-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -3927,10 +3950,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"description": "Must be set to 2024-08-13",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-08-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -3986,10 +4010,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"description": "Must be set to 2024-08-13",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-08-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4044,10 +4069,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"description": "Must be set to 2024-08-13",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-08-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4103,10 +4129,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"description": "Must be set to 2024-08-13",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-08-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4152,10 +4179,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"description": "Must be set to 2024-08-13",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-08-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4219,10 +4247,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"description": "Must be set to 2024-08-13",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-08-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4268,10 +4297,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"description": "Must be set to 2024-08-13",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-08-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4328,10 +4358,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"description": "Must be set to 2024-08-13",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-08-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4711,7 +4742,11 @@
|
||||
"name": "app",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "Conferencing application type",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"google-meet"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -4750,7 +4785,12 @@
|
||||
"name": "app",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "Conferencing application type",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"zoom",
|
||||
"msteams"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -4805,7 +4845,12 @@
|
||||
"name": "app",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "Conferencing application type",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"zoom",
|
||||
"msteams"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -4859,7 +4904,14 @@
|
||||
"name": "app",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "Conferencing application type",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"google-meet",
|
||||
"zoom",
|
||||
"msteams",
|
||||
"daily-video"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -4912,7 +4964,13 @@
|
||||
"name": "app",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "Conferencing application type",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"google-meet",
|
||||
"zoom",
|
||||
"msteams"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -4974,10 +5032,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-06-14`",
|
||||
"description": "Must be set to 2024-06-14",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-06-14"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5023,10 +5082,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-06-14`",
|
||||
"description": "Must be set to 2024-06-14",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-06-14"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5100,10 +5160,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-06-14`",
|
||||
"description": "Must be set to 2024-06-14",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-06-14"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5147,10 +5208,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-06-14`",
|
||||
"description": "Must be set to 2024-06-14",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-06-14"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5204,10 +5266,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-06-14`",
|
||||
"description": "Must be set to 2024-06-14",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-06-14"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5500,8 +5563,6 @@
|
||||
"/v2/oauth-clients": {
|
||||
"post": {
|
||||
"operationId": "OAuthClientsController_createOAuthClient",
|
||||
"summary": "",
|
||||
"description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -5531,8 +5592,6 @@
|
||||
},
|
||||
"get": {
|
||||
"operationId": "OAuthClientsController_getOAuthClients",
|
||||
"summary": "",
|
||||
"description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -5554,8 +5613,6 @@
|
||||
"/v2/oauth-clients/{clientId}": {
|
||||
"get": {
|
||||
"operationId": "OAuthClientsController_getOAuthClientById",
|
||||
"summary": "",
|
||||
"description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
@@ -5584,8 +5641,6 @@
|
||||
},
|
||||
"patch": {
|
||||
"operationId": "OAuthClientsController_updateOAuthClient",
|
||||
"summary": "",
|
||||
"description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
@@ -5624,8 +5679,6 @@
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "OAuthClientsController_deleteOAuthClient",
|
||||
"summary": "",
|
||||
"description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
@@ -5656,8 +5709,6 @@
|
||||
"/v2/oauth-clients/{clientId}/managed-users": {
|
||||
"get": {
|
||||
"operationId": "OAuthClientsController_getOAuthClientManagedUsersById",
|
||||
"summary": "",
|
||||
"description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
@@ -5723,10 +5774,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-06-11`",
|
||||
"description": "Must be set to 2024-06-11",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-06-11"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -5773,10 +5825,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-06-11`",
|
||||
"description": "Must be set to 2024-06-11",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-06-11"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -5815,10 +5868,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-06-11`",
|
||||
"description": "Must be set to 2024-06-11",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-06-11"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -5856,10 +5910,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-06-11`",
|
||||
"description": "Must be set to 2024-06-11",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-06-11"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5903,10 +5958,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-06-11`",
|
||||
"description": "Must be set to 2024-06-11",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-06-11"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5960,10 +6016,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-06-11`",
|
||||
"description": "Must be set to 2024-06-11",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-06-11"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -6086,10 +6143,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-09-04`",
|
||||
"description": "Must be set to 2024-09-04",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-09-04"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -6266,10 +6324,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-09-04`",
|
||||
"description": "Must be set to 2024-09-04",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-09-04"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -6308,10 +6367,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-09-04`",
|
||||
"description": "Must be set to 2024-09-04",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-09-04"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -6346,10 +6406,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-09-04`",
|
||||
"description": "Must be set to 2024-09-04",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-09-04"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -6393,10 +6454,11 @@
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-09-04`",
|
||||
"description": "Must be set to 2024-09-04",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "2024-09-04"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -8236,7 +8298,7 @@
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking",
|
||||
"description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking. Special slug is `attendeePhoneNumber` - if you create\n a phone input field with this slug for organization team event type you can create an organization team event type that can be booked using phone without requiring an email by setting {\"type\": \"email\", \"required\": false, \"hidden\": true} to the email booking field input in the request body.",
|
||||
"example": "some-slug"
|
||||
},
|
||||
"label": {
|
||||
@@ -9082,7 +9144,12 @@
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"type": "object"
|
||||
"type": "object",
|
||||
"description": "Can be set to false only for organization team event types and if you also pass booking field {type: \"phone\", slug: \"attendeePhoneNumber\", required: true, hidden: false, label: \"whatever label\"} of booking field type PhoneFieldInput_2024_06_14 - this is done\n to enable phone only bookings where during the booking attendee can provide only their phone number and not provide email, so you must pass to the email booking field {hidden: true, required: false}.\n If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "Can be set to true only for organization team event types and if you also pass booking field {type: \"phone\", slug: \"attendeePhoneNumber\", required: true, hidden: false, label: \"whatever label\"} of booking field type PhoneFieldInput_2024_06_14 - this is done\n to enable phone only bookings where during the booking attendee can provide only their phone number and not provide email, so you must pass to the email booking field {hidden: true, required: false}.\n If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
@@ -9095,7 +9162,6 @@
|
||||
"required": [
|
||||
"type",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
]
|
||||
},
|
||||
@@ -9818,7 +9884,12 @@
|
||||
},
|
||||
"required": {
|
||||
"type": "object",
|
||||
"default": true
|
||||
"default": true,
|
||||
"description": "Can be set to false only for organization team event types and if you also pass booking field {type: \"phone\", slug: \"attendeePhoneNumber\", required: true, hidden: false, label: \"whatever label\"} of booking field type PhoneFieldInput_2024_06_14 - this is done\n to enable phone only bookings where during the booking attendee can provide only their phone number and not provide email, so you must pass to the email booking field {hidden: true, required: false}.\n If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both. Can only be hidden\n for organization team event types when also providing attendee phone number booking field."
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
@@ -9840,7 +9911,6 @@
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"required",
|
||||
"isDefault",
|
||||
"slug"
|
||||
]
|
||||
@@ -10618,7 +10688,7 @@
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking",
|
||||
"description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking. Special slug is `attendeePhoneNumber` - if you create\n a phone input field with this slug for organization team event type you can create an organization team event type that can be booked using phone without requiring an email by setting {\"type\": \"email\", \"required\": false, \"hidden\": true} to the email booking field input in the request body.",
|
||||
"example": "some-slug"
|
||||
},
|
||||
"label": {
|
||||
@@ -16453,6 +16523,19 @@
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"BookingInputOrganizersDefaultAppLocation_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "organizersDefaultApp",
|
||||
"description": "only available for team event types and the only allowed value for type is `organizersDefaultApp` - it refers to the default app defined by the organizer."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"ValidateBookingLocation_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -16602,6 +16685,9 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/BookingInputPhoneLocation_2024_08_13"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/BookingInputOrganizersDefaultAppLocation_2024_08_13"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -16692,6 +16778,9 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/BookingInputPhoneLocation_2024_08_13"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/BookingInputOrganizersDefaultAppLocation_2024_08_13"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -16788,6 +16877,9 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/BookingInputPhoneLocation_2024_08_13"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/BookingInputOrganizersDefaultAppLocation_2024_08_13"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -8,4 +8,8 @@ export const generateMetadata = async () =>
|
||||
(t) => t("create_your_org_description")
|
||||
);
|
||||
|
||||
export default EnterprisePage;
|
||||
const ServerPageWrapper = async () => {
|
||||
return <EnterprisePage />;
|
||||
};
|
||||
|
||||
export default ServerPageWrapper;
|
||||
|
||||
+1
-1
@@ -174,7 +174,7 @@ const organizationAdminKeys = [
|
||||
|
||||
const useTabs = ({ isDelegationCredentialEnabled }: { isDelegationCredentialEnabled: boolean }) => {
|
||||
const session = useSession();
|
||||
const { data: user } = trpc.viewer.me.useQuery({ includePasswordAdded: true });
|
||||
const { data: user } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
|
||||
const orgBranding = useOrgBranding();
|
||||
const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN;
|
||||
const isOrgAdminOrOwner =
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
|
||||
|
||||
import { createTRPCReact } from "@trpc/react-query";
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useState } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { ENDPOINTS } from "@calcom/trpc/react/shared";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
|
||||
|
||||
import { httpBatchLink, httpLink, loggerLink, splitLink, TRPCClientError } from "@trpc/client";
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ async function postHandler(request: NextRequest) {
|
||||
locale: true,
|
||||
timeZone: true,
|
||||
destinationCalendar: true,
|
||||
isPlatformManaged: true,
|
||||
platformOAuthClients: { select: { id: true, areEmailsEnabled: true } },
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
@@ -70,11 +72,18 @@ async function postHandler(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
const bookingsToRemind = bookings.filter(
|
||||
(booking) =>
|
||||
!booking.user ||
|
||||
!booking.user.isPlatformManaged ||
|
||||
(booking.user.isPlatformManaged && Boolean(booking.user.platformOAuthClients?.[0]?.areEmailsEnabled))
|
||||
);
|
||||
|
||||
const reminders = await prisma.reminderMail.findMany({
|
||||
where: {
|
||||
reminderType: ReminderType.PENDING_BOOKING_CONFIRMATION,
|
||||
referenceId: {
|
||||
in: bookings.map((b) => b.id),
|
||||
in: bookingsToRemind.map((b) => b.id),
|
||||
},
|
||||
elapsedMinutes: {
|
||||
gte: interval,
|
||||
@@ -82,7 +91,7 @@ async function postHandler(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
for (const booking of bookings.filter((b) => !reminders.some((r) => r.referenceId == b.id))) {
|
||||
for (const booking of bookingsToRemind.filter((b) => !reminders.some((r) => r.referenceId == b.id))) {
|
||||
const { user } = booking;
|
||||
const name = user?.name || user?.username;
|
||||
if (!user || !name || !user.timeZone) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CALCOM_VERSION } from "@calcom/lib/constants";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export function useViewerI18n(locale: string) {
|
||||
return trpc.viewer.public.i18n.useQuery(
|
||||
return trpc.viewer.i18n.get.useQuery(
|
||||
{ locale, CalComVersion: CALCOM_VERSION },
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ interface AdditionalCalendarSelectorProps {
|
||||
|
||||
const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
||||
const { t } = useLocale();
|
||||
const [data] = trpc.viewer.integrations.useSuspenseQuery({ variant: "calendar", onlyInstalled: true });
|
||||
const [data] = trpc.viewer.apps.integrations.useSuspenseQuery({ variant: "calendar", onlyInstalled: true });
|
||||
|
||||
const options = data.items.map((item) => ({
|
||||
label: item.name,
|
||||
|
||||
@@ -87,7 +87,7 @@ export const AppPage = ({
|
||||
if (data?.setupPending) return;
|
||||
setIsLoading(false);
|
||||
showToast(data?.message || t("app_successfully_installed"), "success");
|
||||
await utils.viewer.appCredentialsByType.invalidate({ appType: type });
|
||||
await utils.viewer.apps.appCredentialsByType.invalidate({ appType: type });
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
|
||||
@@ -143,7 +143,7 @@ export const AppPage = ({
|
||||
*/
|
||||
const [appInstalledForAllTargets, setAppInstalledForAllTargets] = useState(false);
|
||||
|
||||
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery({ appType: type });
|
||||
const appDbQuery = trpc.viewer.apps.appCredentialsByType.useQuery({ appType: type });
|
||||
|
||||
useEffect(
|
||||
function refactorMeWithoutEffect() {
|
||||
@@ -161,7 +161,7 @@ export const AppPage = ({
|
||||
[appDbQuery.data, availableForTeams]
|
||||
);
|
||||
|
||||
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
|
||||
const dependencyData = trpc.viewer.apps.queryForDependencies.useQuery(dependencies, {
|
||||
enabled: !!dependencies,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,16 +6,14 @@ import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { DestinationCalendarSettingsWebWrapper } from "@calcom/atoms/destination-calendar/wrappers/DestinationCalendarSettingsWebWrapper";
|
||||
import { SelectedCalendarsSettingsWebWrapper } from "@calcom/atoms/selected-calendars/wrappers/SelectedCalendarsSettingsWebWrapper";
|
||||
import AppListCard from "@calcom/features/apps/components/AppListCard";
|
||||
import { SkeletonLoader } from "@calcom/features/apps/components/SkeletonLoader";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
EmptyScreen,
|
||||
List,
|
||||
ShellSubHeading,
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
import { ShellSubHeading } from "@calcom/ui/components/layout";
|
||||
import { List } from "@calcom/ui/components/list";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
@@ -31,7 +29,7 @@ type Props = {
|
||||
|
||||
function CalendarList(props: Props) {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
||||
const query = trpc.viewer.apps.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
||||
|
||||
return (
|
||||
<QueryCell
|
||||
@@ -80,7 +78,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
const utils = trpc.useUtils();
|
||||
const onChanged = () =>
|
||||
Promise.allSettled([
|
||||
utils.viewer.integrations.invalidate(
|
||||
utils.viewer.apps.integrations.invalidate(
|
||||
{ variant: "calendar", onlyInstalled: true },
|
||||
{
|
||||
exact: true,
|
||||
@@ -89,7 +87,10 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
utils.viewer.connectedCalendars.invalidate(),
|
||||
]);
|
||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
||||
const installedCalendars = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
||||
const installedCalendars = trpc.viewer.apps.integrations.useQuery({
|
||||
variant: "calendar",
|
||||
onlyInstalled: true,
|
||||
});
|
||||
const mutation = trpc.viewer.setDestinationCalendar.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.viewer.connectedCalendars.invalidate();
|
||||
|
||||
@@ -11,7 +11,7 @@ export const InstallAppButtonChild = ({
|
||||
...props
|
||||
}: {
|
||||
multiInstall?: boolean;
|
||||
credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"];
|
||||
credentials?: RouterOutputs["viewer"]["apps"]["appCredentialsByType"]["credentials"];
|
||||
paid?: AppFrontendPayload["paid"];
|
||||
} & ButtonProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import type { inferRouterOutputs } from "@trpc/server";
|
||||
|
||||
type RouterOutput = inferRouterOutputs<AppRouter>;
|
||||
type Credentials = RouterOutput["viewer"]["appCredentialsByType"]["credentials"];
|
||||
type Credentials = RouterOutput["viewer"]["apps"]["appCredentialsByType"]["credentials"];
|
||||
|
||||
interface Props {
|
||||
credentials: Credentials;
|
||||
@@ -48,7 +48,7 @@ export function MultiDisconnectIntegration({ credentials, onSuccess }: Props) {
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.connectedCalendars.invalidate();
|
||||
await utils.viewer.integrations.invalidate();
|
||||
await utils.viewer.apps.integrations.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -903,33 +903,23 @@ type NoShowProps = {
|
||||
};
|
||||
|
||||
const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => {
|
||||
const { email, name, bookingUid, isBookingInPast, noShow: noShowAttendee, phoneNumber } = attendeeProps;
|
||||
const { email, name, bookingUid, isBookingInPast, noShow, phoneNumber } = attendeeProps;
|
||||
const { t } = useLocale();
|
||||
|
||||
const [noShow, setNoShow] = useState(noShowAttendee);
|
||||
const utils = trpc.useUtils();
|
||||
const [openDropdown, setOpenDropdown] = useState(false);
|
||||
const { copyToClipboard, isCopied } = useCopy();
|
||||
|
||||
const noShowMutation = trpc.viewer.markNoShow.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
showToast(data.message, "success");
|
||||
utils.viewer.bookings.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
showToast(err.message, "error");
|
||||
},
|
||||
});
|
||||
|
||||
function toggleNoShow({
|
||||
attendee,
|
||||
bookingUid,
|
||||
}: {
|
||||
attendee: { email: string; noShow: boolean };
|
||||
bookingUid: string;
|
||||
}) {
|
||||
noShowMutation.mutate({ bookingUid, attendees: [attendee] });
|
||||
setNoShow(!noShow);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown open={openDropdown} onOpenChange={setOpenDropdown}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -978,29 +968,16 @@ const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => {
|
||||
|
||||
{isBookingInPast && (
|
||||
<DropdownMenuItem className="focus:outline-none">
|
||||
{noShow ? (
|
||||
<DropdownItem
|
||||
data-testid="unmark-no-show"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOpenDropdown(false);
|
||||
toggleNoShow({ attendee: { noShow: false, email }, bookingUid });
|
||||
}}
|
||||
StartIcon="eye">
|
||||
{t("unmark_as_no_show")}
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<DropdownItem
|
||||
data-testid="mark-no-show"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOpenDropdown(false);
|
||||
toggleNoShow({ attendee: { noShow: true, email }, bookingUid });
|
||||
}}
|
||||
StartIcon="eye-off">
|
||||
{t("mark_as_no_show")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem
|
||||
data-testid={noShow ? "unmark-no-show" : "mark-no-show"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOpenDropdown(false);
|
||||
noShowMutation.mutate({ bookingUid, attendees: [{ noShow: !noShow, email }] });
|
||||
}}
|
||||
StartIcon={noShow ? "eye" : "eye-off"}>
|
||||
{noShow ? t("unmark_as_no_show") : t("mark_as_no_show")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -25,7 +25,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
|
||||
const { title, logo, type, installed, isDefault, defaultInstall, slug } = props;
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const setDefaultConferencingApp = trpc.viewer.appsRouter.setDefaultConferencingApp.useMutation({
|
||||
const setDefaultConferencingApp = trpc.viewer.apps.setDefaultConferencingApp.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.me.invalidate();
|
||||
},
|
||||
@@ -54,7 +54,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
|
||||
setDefaultConferencingApp.mutate({ slug });
|
||||
}
|
||||
setInstalling(false);
|
||||
utils.viewer.integrations.invalidate();
|
||||
utils.viewer.apps.integrations.invalidate();
|
||||
showToast(t("app_successfully_installed"), "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||
eventTypeId: null,
|
||||
});
|
||||
const { t } = useLocale();
|
||||
const queryIntegrations = trpc.viewer.integrations.useQuery({
|
||||
const queryIntegrations = trpc.viewer.apps.integrations.useQuery({
|
||||
variant: "calendar",
|
||||
onlyInstalled: false,
|
||||
sortByMostPopular: true,
|
||||
|
||||
@@ -14,7 +14,7 @@ interface ConnectedAppStepProps {
|
||||
|
||||
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||
const { nextStep } = props;
|
||||
const { data: queryConnectedVideoApps, isPending } = trpc.viewer.integrations.useQuery({
|
||||
const { data: queryConnectedVideoApps, isPending } = trpc.viewer.apps.integrations.useQuery({
|
||||
variant: "conferencing",
|
||||
onlyInstalled: false,
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Schedule } from "@calcom/features/schedules";
|
||||
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
|
||||
import { Button, Form } from "@calcom/ui";
|
||||
|
||||
import type { TRPCClientErrorLike } from "@trpc/client";
|
||||
|
||||
@@ -15,7 +15,7 @@ type FormData = {
|
||||
};
|
||||
|
||||
const UserProfile = () => {
|
||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||
const [user] = trpc.viewer.me.get.useSuspenseQuery();
|
||||
const { t } = useLocale();
|
||||
const avatarRef = useRef<HTMLInputElement>(null);
|
||||
const { setValue, handleSubmit, getValues } = useForm<FormData>({
|
||||
@@ -56,7 +56,7 @@ const UserProfile = () => {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
await utils.viewer.me.refetch();
|
||||
await utils.viewer.me.get.refetch();
|
||||
const redirectUrl = localStorage.getItem("onBoardingRedirect");
|
||||
localStorage.removeItem("onBoardingRedirect");
|
||||
redirectUrl ? router.push(redirectUrl) : router.push("/");
|
||||
|
||||
@@ -21,7 +21,7 @@ interface IUserSettingsProps {
|
||||
|
||||
const UserSettings = (props: IUserSettingsProps) => {
|
||||
const { nextStep } = props;
|
||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||
const [user] = trpc.viewer.me.get.useSuspenseQuery();
|
||||
const { t } = useLocale();
|
||||
const { setTimezone: setSelectedTimeZone, timezone: selectedTimeZone } = useTimePreferences();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export function usePlatformMe() {
|
||||
const meQuery = trpc.viewer.platformMe.useQuery(undefined, {
|
||||
const meQuery = trpc.viewer.me.platformMe.useQuery(undefined, {
|
||||
retry(failureCount) {
|
||||
return failureCount > 3;
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, Input, Label } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
@@ -64,7 +64,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
onErrorMutation,
|
||||
readonly: disabled,
|
||||
} = props;
|
||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||
const [user] = trpc.viewer.me.get.useSuspenseQuery();
|
||||
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
|
||||
const [markAsError, setMarkAsError] = useState(false);
|
||||
const recentAttemptPaymentStatus = searchParams?.get("recentAttemptPaymentStatus");
|
||||
|
||||
@@ -9,7 +9,7 @@ import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
|
||||
import { Button, Dialog, DialogClose, DialogContent, TextField, DialogFooter, Tooltip } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { WEBSITE_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
|
||||
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
|
||||
@@ -47,7 +47,7 @@ export const UsernameAvailabilityField = ({
|
||||
onErrorMutation,
|
||||
}: UsernameAvailabilityFieldProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||
const [user] = trpc.viewer.me.get.useSuspenseQuery();
|
||||
const [currentUsernameState, setCurrentUsernameState] = useState(user.username || "");
|
||||
const { username: usernameFromQuery, setQuery: setUsernameFromQuery } = useRouterQuery("username");
|
||||
const { username: currentUsername, setQuery: setCurrentUsername } =
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { z } from "zod";
|
||||
@@ -78,7 +78,7 @@ const getAppBySlug = async (appSlug: string) => {
|
||||
};
|
||||
|
||||
const getEventTypes = async (userId: number, teamIds?: number[]) => {
|
||||
const eventTypeSelect: Prisma.EventTypeSelect = {
|
||||
const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
||||
id: true,
|
||||
description: true,
|
||||
durationLimits: true,
|
||||
@@ -99,7 +99,7 @@ const getEventTypes = async (userId: number, teamIds?: number[]) => {
|
||||
userId: true,
|
||||
destinationCalendar: true,
|
||||
bookingFields: true,
|
||||
};
|
||||
});
|
||||
let eventTypeGroups: TEventTypeGroup[] | null = [];
|
||||
|
||||
if (teamIds && teamIds.length > 0) {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
await ssr.viewer.me.prefetch();
|
||||
await ssr.viewer.me.get.prefetch();
|
||||
|
||||
const user = await UserRepository.findUserTeams({
|
||||
id: session.user.id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export function useMeQuery() {
|
||||
const meQuery = trpc.viewer.me.useQuery(undefined, {
|
||||
const meQuery = trpc.viewer.me.get.useQuery(undefined, {
|
||||
retry(failureCount) {
|
||||
return failureCount > 3;
|
||||
},
|
||||
|
||||
@@ -3,17 +3,14 @@
|
||||
import type { ChangeEventHandler } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { AllApps } from "@calcom/features/apps/components/AllApps";
|
||||
import { AppStoreCategories } from "@calcom/features/apps/components/Categories";
|
||||
import { PopularAppsSlider } from "@calcom/features/apps/components/PopularAppsSlider";
|
||||
import { RecentAppsSlider } from "@calcom/features/apps/components/RecentAppsSlider";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import type { HorizontalTabItemProps } from "@calcom/ui";
|
||||
import {
|
||||
AllApps,
|
||||
AppStoreCategories,
|
||||
HorizontalTabs,
|
||||
TextField,
|
||||
PopularAppsSlider,
|
||||
RecentAppsSlider,
|
||||
} from "@calcom/ui";
|
||||
import { HorizontalTabs, TextField } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { AppCard } from "@calcom/features/apps/components/AppCard";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { AppCard, SkeletonText } from "@calcom/ui";
|
||||
import { SkeletonText } from "@calcom/ui";
|
||||
|
||||
import type { CategoryDataProps } from "@lib/apps/categories/[category]/getStaticProps";
|
||||
|
||||
|
||||
@@ -7,18 +7,13 @@ import { AppList, type HandleDisconnect } from "@calcom/features/apps/components
|
||||
import type { UpdateUsersDefaultConferencingAppParams } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
||||
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||
import type { RemoveAppParams } from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||
import { SkeletonLoader } from "@calcom/features/apps/components/SkeletonLoader";
|
||||
import type { BulkUpdatParams } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { Icon } from "@calcom/ui";
|
||||
import {
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
Button,
|
||||
EmptyScreen,
|
||||
ShellSubHeading,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { Button, EmptyScreen, ShellSubHeading, showToast } from "@calcom/ui";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
@@ -38,16 +33,16 @@ const IntegrationsContainer = ({
|
||||
}: IntegrationsContainerProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const query = trpc.viewer.integrations.useQuery({
|
||||
const query = trpc.viewer.apps.integrations.useQuery({
|
||||
variant,
|
||||
exclude,
|
||||
onlyInstalled: true,
|
||||
includeTeamInstalledApps: true,
|
||||
});
|
||||
|
||||
const { data: defaultConferencingApp } = trpc.viewer.getUsersDefaultConferencingApp.useQuery();
|
||||
const { data: defaultConferencingApp } = trpc.viewer.apps.getUsersDefaultConferencingApp.useQuery();
|
||||
|
||||
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation();
|
||||
const updateDefaultAppMutation = trpc.viewer.apps.updateUserDefaultConferencingApp.useMutation();
|
||||
|
||||
const updateLocationsMutation = trpc.viewer.eventTypes.bulkUpdateToDefaultLocation.useMutation();
|
||||
|
||||
@@ -65,7 +60,7 @@ const IntegrationsContainer = ({
|
||||
{
|
||||
onSuccess: () => {
|
||||
showToast("Default app updated successfully", "success");
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
utils.viewer.apps.getUsersDefaultConferencingApp.invalidate();
|
||||
onSuccessCallback();
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -83,7 +78,7 @@ const IntegrationsContainer = ({
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
utils.viewer.apps.getUsersDefaultConferencingApp.invalidate();
|
||||
callback();
|
||||
},
|
||||
}
|
||||
@@ -91,11 +86,11 @@ const IntegrationsContainer = ({
|
||||
};
|
||||
|
||||
const handleConnectDisconnectIntegrationMenuToggle = () => {
|
||||
utils.viewer.integrations.invalidate();
|
||||
utils.viewer.apps.integrations.invalidate();
|
||||
};
|
||||
|
||||
const handleBulkEditDialogToggle = () => {
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
utils.viewer.apps.getUsersDefaultConferencingApp.invalidate();
|
||||
};
|
||||
|
||||
// TODO: Refactor and reuse getAppCategories?
|
||||
@@ -218,7 +213,7 @@ export default function InstalledApps({ category }: PageProps) {
|
||||
onSuccess: () => {
|
||||
showToast(t("app_removed_successfully"), "success");
|
||||
callback();
|
||||
utils.viewer.integrations.invalidate();
|
||||
utils.viewer.apps.integrations.invalidate();
|
||||
utils.viewer.connectedCalendars.invalidate();
|
||||
},
|
||||
onError: () => {
|
||||
|
||||
@@ -70,7 +70,7 @@ export const AvailabilitySettingsWebWrapper = ({
|
||||
};
|
||||
|
||||
const handleBulkEditDialogToggle = () => {
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
utils.viewer.apps.getUsersDefaultConferencingApp.invalidate();
|
||||
};
|
||||
|
||||
const isDefaultSchedule = me.data?.defaultScheduleId === scheduleId;
|
||||
|
||||
@@ -15,19 +15,18 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import type { OrganizationRepository } from "@calcom/lib/server/repository/organization";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { EmptyScreen, showToast, ToggleGroup } from "@calcom/ui";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
||||
export function AvailabilityList() {
|
||||
const { t } = useLocale();
|
||||
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
|
||||
const utils = trpc.useUtils();
|
||||
const { data: availabilityData, isFetching: isFetchingAvailabilityData } =
|
||||
trpc.viewer.availability.list.useQuery();
|
||||
|
||||
const meQuery = trpc.viewer.me.useQuery();
|
||||
const meQuery = trpc.viewer.me.get.useQuery();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -101,7 +100,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
};
|
||||
|
||||
const handleBulkEditDialogToggle = () => {
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
utils.viewer.apps.getUsersDefaultConferencingApp.invalidate();
|
||||
};
|
||||
|
||||
const duplicateMutation = trpc.viewer.availability.schedule.duplicate.useMutation({
|
||||
@@ -121,9 +120,13 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
|
||||
const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
|
||||
|
||||
if (isFetchingAvailabilityData || !availabilityData) {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{schedules.length === 0 ? (
|
||||
{availabilityData.schedules.length === 0 ? (
|
||||
<div className="flex justify-center">
|
||||
<EmptyScreen
|
||||
Icon="clock"
|
||||
@@ -137,7 +140,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
<>
|
||||
<div className="border-subtle bg-default overflow-hidden rounded-md border">
|
||||
<ul className="divide-subtle divide-y" data-testid="schedules" ref={animationParentRef}>
|
||||
{schedules.map((schedule) => (
|
||||
{availabilityData.schedules.map((schedule) => (
|
||||
<ScheduleListItem
|
||||
displayOptions={{
|
||||
hour12: meQuery.data?.timeFormat ? meQuery.data.timeFormat === 12 : undefined,
|
||||
@@ -146,7 +149,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
}}
|
||||
key={schedule.id}
|
||||
schedule={schedule}
|
||||
isDeletable={schedules.length !== 1}
|
||||
isDeletable={availabilityData.schedules.length !== 1}
|
||||
updateDefault={updateMutation.mutate}
|
||||
deleteFunction={deleteMutation.mutate}
|
||||
duplicateFunction={duplicateMutation.mutate}
|
||||
@@ -178,18 +181,6 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
);
|
||||
}
|
||||
|
||||
function AvailabilityListWithQuery() {
|
||||
const query = trpc.viewer.availability.list.useQuery();
|
||||
|
||||
return (
|
||||
<QueryCell
|
||||
query={query}
|
||||
success={({ data }) => <AvailabilityList {...data} />}
|
||||
customLoader={<SkeletonLoader />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type PageProps = {
|
||||
currentOrg?: Awaited<ReturnType<typeof OrganizationRepository.findCurrentOrg>> | null;
|
||||
};
|
||||
@@ -245,6 +236,7 @@ export default function AvailabilityPage({ currentOrg }: PageProps) {
|
||||
const { t } = useLocale();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const me = useMeQuery();
|
||||
|
||||
const { data: _data } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { enabled: !currentOrg });
|
||||
const data = currentOrg ?? _data;
|
||||
|
||||
@@ -264,6 +256,6 @@ export default function AvailabilityPage({ currentOrg }: PageProps) {
|
||||
return searchParams?.get("type") === "team" && canViewTeamAvailability ? (
|
||||
<AvailabilitySliderTable userTimeFormat={me?.data?.timeFormat ?? null} isOrg={isOrg} />
|
||||
) : (
|
||||
<AvailabilityListWithQuery />
|
||||
<AvailabilityList />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
let userTimeFormat: number | null = null;
|
||||
let requiresLoginToUpdate = false;
|
||||
if (session) {
|
||||
const user = await ssr.viewer.me.fetch();
|
||||
const user = await ssr.viewer.me.get.fetch();
|
||||
tz = user.timeZone;
|
||||
userTimeFormat = user.timeFormat;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/emb
|
||||
import { EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
|
||||
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
|
||||
import { InfiniteSkeletonLoader } from "@calcom/features/eventtypes/components/SkeletonLoader";
|
||||
import {
|
||||
InfiniteSkeletonLoader,
|
||||
EventTypesSkeletonLoader,
|
||||
} from "@calcom/features/eventtypes/components/SkeletonLoader";
|
||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
import { parseEventTypeColor } from "@calcom/lib";
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
@@ -911,7 +914,7 @@ const InfiniteScrollMain = ({
|
||||
}
|
||||
|
||||
if (!eventTypeGroups || !profiles || status === "pending") {
|
||||
return <InfiniteSkeletonLoader />;
|
||||
return <EventTypesSkeletonLoader />;
|
||||
}
|
||||
|
||||
const tabs = eventTypeGroups.map((item) => ({
|
||||
|
||||
@@ -89,7 +89,7 @@ const OnboardingPage = (props: PageProps) => {
|
||||
const params = useParamsWithFallback();
|
||||
|
||||
const router = useRouter();
|
||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||
const [user] = trpc.viewer.me.get.useSuspenseQuery();
|
||||
const { t } = useLocale();
|
||||
|
||||
const result = stepRouteSchema.safeParse({
|
||||
|
||||
@@ -85,7 +85,7 @@ const AppearanceView = ({
|
||||
user,
|
||||
hasPaidPlan,
|
||||
}: {
|
||||
user: RouterOutputs["viewer"]["me"];
|
||||
user: RouterOutputs["viewer"]["me"]["get"];
|
||||
hasPaidPlan: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
@@ -436,7 +436,7 @@ const AppearanceView = ({
|
||||
};
|
||||
|
||||
const AppearancePage = () => {
|
||||
const { data: user, isPending } = trpc.viewer.me.useQuery();
|
||||
const { data: user, isPending } = trpc.viewer.me.get.useQuery();
|
||||
const { isPending: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan();
|
||||
|
||||
if (isPending || isTeamPlanStatusLoading || !user) return <SkeletonLoader />;
|
||||
|
||||
@@ -66,7 +66,7 @@ const SkeletonLoader = () => {
|
||||
|
||||
interface GeneralViewProps {
|
||||
localeProp: string;
|
||||
user: RouterOutputs["viewer"]["me"];
|
||||
user: RouterOutputs["viewer"]["me"]["get"];
|
||||
travelSchedules: RouterOutputs["viewer"]["getTravelSchedules"];
|
||||
revalidatePage: GeneralQueryViewProps["revalidatePage"];
|
||||
}
|
||||
@@ -78,7 +78,7 @@ type GeneralQueryViewProps = {
|
||||
const GeneralQueryView = ({ revalidatePage }: GeneralQueryViewProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const { data: user, isPending } = trpc.viewer.me.useQuery();
|
||||
const { data: user, isPending } = trpc.viewer.me.get.useQuery();
|
||||
|
||||
const { data: travelSchedules, isPending: isPendingTravelSchedules } =
|
||||
trpc.viewer.getTravelSchedules.useQuery();
|
||||
|
||||
@@ -21,7 +21,7 @@ import turndown from "@calcom/lib/turndownService";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -95,13 +95,13 @@ const ProfileView = () => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const { update } = useSession();
|
||||
const { data: user, isPending } = trpc.viewer.me.useQuery({ includePasswordAdded: true });
|
||||
const { data: user, isPending } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
|
||||
|
||||
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async (res) => {
|
||||
await update(res);
|
||||
utils.viewer.me.invalidate();
|
||||
utils.viewer.shouldVerifyEmail.invalidate();
|
||||
utils.viewer.me.shouldVerifyEmail.invalidate();
|
||||
|
||||
if (res.hasEmailBeenChanged && res.sendEmailVerification) {
|
||||
showToast(t("change_of_email_toast", { email: tempFormValues?.email }), "success");
|
||||
@@ -187,14 +187,14 @@ const ProfileView = () => {
|
||||
setHasDeleteErrors(true);
|
||||
setDeleteErrorMessage(errorMessages[error.message]);
|
||||
};
|
||||
const deleteMeMutation = trpc.viewer.deleteMe.useMutation({
|
||||
const deleteMeMutation = trpc.viewer.me.deleteMe.useMutation({
|
||||
onSuccess: onDeleteMeSuccessMutation,
|
||||
onError: onDeleteMeErrorMutation,
|
||||
async onSettled() {
|
||||
await utils.viewer.me.invalidate();
|
||||
},
|
||||
});
|
||||
const deleteMeWithoutPasswordMutation = trpc.viewer.deleteMeWithoutPassword.useMutation({
|
||||
const deleteMeWithoutPasswordMutation = trpc.viewer.me.deleteMeWithoutPassword.useMutation({
|
||||
onSuccess: onDeleteMeSuccessMutation,
|
||||
onError: onDeleteMeErrorMutation,
|
||||
async onSettled() {
|
||||
@@ -511,8 +511,8 @@ const ProfileForm = ({
|
||||
extraField?: React.ReactNode;
|
||||
isPending: boolean;
|
||||
isFallbackImg: boolean;
|
||||
user: RouterOutputs["viewer"]["me"];
|
||||
userOrganization: RouterOutputs["viewer"]["me"]["organization"];
|
||||
user: RouterOutputs["viewer"]["me"]["get"];
|
||||
userOrganization: RouterOutputs["viewer"]["me"]["get"]["organization"];
|
||||
isCALIdentityProvider: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -17,7 +17,7 @@ const SkeletonLoader = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"] }) => {
|
||||
const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"]["get"] }) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const [disableImpersonation, setDisableImpersonation] = useState<boolean | undefined>(
|
||||
@@ -32,8 +32,8 @@ const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"
|
||||
utils.viewer.me.invalidate();
|
||||
},
|
||||
onMutate: async ({ disableImpersonation }) => {
|
||||
await utils.viewer.me.cancel();
|
||||
const previousValue = utils.viewer.me.getData();
|
||||
await utils.viewer.me.get.cancel();
|
||||
const previousValue = utils.viewer.me.get.getData();
|
||||
|
||||
setDisableImpersonation(disableImpersonation);
|
||||
|
||||
@@ -41,7 +41,7 @@ const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (context?.previousValue) {
|
||||
utils.viewer.me.setData(undefined, context.previousValue);
|
||||
utils.viewer.me.get.setData(undefined, context.previousValue);
|
||||
setDisableImpersonation(context.previousValue?.disableImpersonation);
|
||||
}
|
||||
showToast(`${t("error")}, ${error.message}`, "error");
|
||||
@@ -68,7 +68,7 @@ const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"
|
||||
};
|
||||
|
||||
const ProfileImpersonationViewWrapper = () => {
|
||||
const { data: user, isPending } = trpc.viewer.me.useQuery();
|
||||
const { data: user, isPending } = trpc.viewer.me.get.useQuery();
|
||||
if (isPending || !user) return <SkeletonLoader />;
|
||||
|
||||
return <ProfileImpersonationView user={user} />;
|
||||
|
||||
@@ -33,7 +33,7 @@ type ChangePasswordSessionFormValues = {
|
||||
};
|
||||
|
||||
interface PasswordViewProps {
|
||||
user: RouterOutputs["viewer"]["me"];
|
||||
user: RouterOutputs["viewer"]["me"]["get"];
|
||||
}
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
@@ -72,12 +72,12 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
||||
utils.viewer.me.invalidate();
|
||||
},
|
||||
onMutate: async () => {
|
||||
await utils.viewer.me.cancel();
|
||||
const previousValue = await utils.viewer.me.getData();
|
||||
await utils.viewer.me.get.cancel();
|
||||
const previousValue = await utils.viewer.me.get.getData();
|
||||
const previousMetadata = userMetadataSchema.safeParse(previousValue?.metadata);
|
||||
|
||||
if (previousValue && sessionTimeout && previousMetadata.success) {
|
||||
utils.viewer.me.setData(undefined, {
|
||||
utils.viewer.me.get.setData(undefined, {
|
||||
...previousValue,
|
||||
metadata: { ...previousMetadata?.data, sessionTimeout: sessionTimeout },
|
||||
});
|
||||
@@ -86,7 +86,7 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
||||
},
|
||||
onError: (error, _, context) => {
|
||||
if (context?.previousValue) {
|
||||
utils.viewer.me.setData(undefined, context.previousValue);
|
||||
utils.viewer.me.get.setData(undefined, context.previousValue);
|
||||
}
|
||||
showToast(`${t("session_timeout_change_error")}, ${error.message}`, "error");
|
||||
},
|
||||
@@ -305,7 +305,7 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
||||
};
|
||||
|
||||
const PasswordViewWrapper = () => {
|
||||
const { data: user, isPending } = trpc.viewer.me.useQuery({ includePasswordAdded: true });
|
||||
const { data: user, isPending } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
|
||||
const { t } = useLocale();
|
||||
if (isPending || !user) return <SkeletonLoader />;
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const TwoFactorAuthView = () => {
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const { t } = useLocale();
|
||||
const { data: user, isPending } = trpc.viewer.me.useQuery({ includePasswordAdded: true });
|
||||
const { data: user, isPending } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
|
||||
|
||||
const [enableModalOpen, setEnableModalOpen] = useState<boolean>(false);
|
||||
const [disableModalOpen, setDisableModalOpen] = useState<boolean>(false);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button } from "@calcom/ui";
|
||||
|
||||
export const TeamsCTA = () => {
|
||||
const { t } = useLocale();
|
||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||
const [user] = trpc.viewer.me.get.useSuspenseQuery();
|
||||
return !user.organizationId || user.organization.isOrgAdmin ? (
|
||||
<Button
|
||||
data-testid="new-team-btn"
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Button, EmptyScreen } from "@calcom/ui";
|
||||
import { showToast } from "@calcom/ui";
|
||||
|
||||
export type OrgUpgradeBannerProps = {
|
||||
data: RouterOutputs["viewer"]["getUserTopBanners"]["orgUpgradeBanner"];
|
||||
data: RouterOutputs["viewer"]["me"]["getUserTopBanners"]["orgUpgradeBanner"];
|
||||
};
|
||||
|
||||
export default function UpgradePage() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.0.19",
|
||||
"version": "5.1.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
||||
import { i18nRouter } from "@calcom/trpc/server/routers/viewer/i18n/_router";
|
||||
|
||||
export default createNextApiHandler(i18nRouter, true, "i18n");
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
||||
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
||||
|
||||
export default createNextApiHandler(meRouter);
|
||||
@@ -14,9 +14,11 @@ test.describe("Forgot password", async () => {
|
||||
|
||||
// Got to reset password flow
|
||||
await page.goto("/auth/forgot-password");
|
||||
await page.waitForSelector("text=Forgot Password?");
|
||||
|
||||
await page.fill('input[name="email"]', `${user.username}@example.com`);
|
||||
await page.press('input[name="email"]', "Enter");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// wait for confirm page.
|
||||
await page.waitForSelector("text=Reset link sent");
|
||||
|
||||
@@ -228,6 +228,8 @@ test.describe("Reschedule for booking with seats", () => {
|
||||
`/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[0].referenceUid}`
|
||||
);
|
||||
|
||||
await expect(page.locator('text="Cancel event"')).toBeVisible();
|
||||
|
||||
await page.locator('[data-testid="cancel_reason"]').fill("Test reason");
|
||||
|
||||
await page.locator('[data-testid="confirm_cancel"]').click();
|
||||
|
||||
@@ -70,6 +70,7 @@ test.describe("Bookings", () => {
|
||||
const firstUser = await users.create();
|
||||
await firstUser.apiLogin();
|
||||
await page.goto(`/bookings/upcoming`);
|
||||
await page.waitForResponse((response) => /\/api\/trpc\/bookings\/get.*/.test(response.url()));
|
||||
|
||||
await page.locator('[data-testid="add-filter-button"]').click();
|
||||
await page.locator('[data-testid="add-filter-item-dateRange"]').click();
|
||||
@@ -233,6 +234,7 @@ test.describe("Bookings", () => {
|
||||
const firstUser = await users.create();
|
||||
await firstUser.apiLogin();
|
||||
await page.goto(`/bookings/past`);
|
||||
await page.waitForResponse((response) => /\/api\/trpc\/bookings\/get.*/.test(response.url()));
|
||||
|
||||
await page.locator('[data-testid="add-filter-button"]').click();
|
||||
await page.locator('[data-testid="add-filter-item-dateRange"]').click();
|
||||
|
||||
@@ -7,6 +7,7 @@ test.describe("Change App Theme Test", () => {
|
||||
const pro = await users.create();
|
||||
await pro.apiLogin();
|
||||
await page.goto("/settings/my-account/appearance");
|
||||
await expect(page.locator('text="Dashboard theme"')).toBeVisible();
|
||||
await page.click('[data-testid="appTheme-dark"]');
|
||||
await page.click('[data-testid="update-app-theme-btn"]');
|
||||
|
||||
@@ -24,6 +25,7 @@ test.describe("Change App Theme Test", () => {
|
||||
const pro = await users.create();
|
||||
await pro.apiLogin();
|
||||
await page.goto("/settings/my-account/appearance");
|
||||
await expect(page.locator('text="Dashboard theme"')).toBeVisible();
|
||||
await page.click('[data-testid="appTheme-light"]');
|
||||
await page.click('[data-testid="update-app-theme-btn"]');
|
||||
|
||||
@@ -42,6 +44,7 @@ test.describe("Change App Theme Test", () => {
|
||||
await pro.apiLogin();
|
||||
|
||||
await page.goto("/settings/my-account/appearance");
|
||||
await expect(page.locator('text="Dashboard theme"')).toBeVisible();
|
||||
await page.click('[data-testid="appTheme-light"]');
|
||||
await page.click('[data-testid="update-app-theme-btn"]');
|
||||
const toast1 = await page.waitForSelector('[data-testid="toast-success"]');
|
||||
@@ -69,6 +72,7 @@ test.describe("Change Booking Page Theme Test", () => {
|
||||
await pro.apiLogin();
|
||||
|
||||
await page.goto("/settings/my-account/appearance");
|
||||
await expect(page.locator('text="Dashboard theme"')).toBeVisible();
|
||||
|
||||
//Click the "Dark" theme label
|
||||
await page.click('[data-testid="theme-dark"]');
|
||||
@@ -90,6 +94,7 @@ test.describe("Change Booking Page Theme Test", () => {
|
||||
await pro.apiLogin();
|
||||
|
||||
await page.goto("/settings/my-account/appearance");
|
||||
await expect(page.locator('text="Dashboard theme"')).toBeVisible();
|
||||
|
||||
//Click the "Light" theme label
|
||||
await page.click('[data-testid="theme-light"]');
|
||||
@@ -109,6 +114,7 @@ test.describe("Change Booking Page Theme Test", () => {
|
||||
await pro.apiLogin();
|
||||
|
||||
await page.goto("/settings/my-account/appearance");
|
||||
await expect(page.locator('text="Dashboard theme"')).toBeVisible();
|
||||
|
||||
await page.click('[data-testid="theme-light"]');
|
||||
await page.click('[data-testid="update-theme-btn"]');
|
||||
|
||||
@@ -1043,6 +1043,8 @@ export async function login(
|
||||
|
||||
//login
|
||||
await page.goto("/");
|
||||
await page.waitForSelector("text=Welcome back");
|
||||
|
||||
await emailLocator.fill(user.email ?? `${user.username}@example.com`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await passwordLocator.fill(user.password ?? user.username!);
|
||||
|
||||
@@ -19,6 +19,7 @@ test.describe("unauthorized user sees correct translations (de)", async () => {
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
await page.waitForSelector("text=Willkommen zurück");
|
||||
const locator = page.getByText("Willkommen zurück", { exact: true });
|
||||
expect(await locator.count()).toEqual(1);
|
||||
}
|
||||
@@ -43,6 +44,7 @@ test.describe("unauthorized user sees correct translations (ar)", async () => {
|
||||
await page.locator("html[dir=rtl]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
await page.waitForSelector("text=أهلاً بك من جديد");
|
||||
const locator = page.getByText("أهلاً بك من جديد", { exact: true });
|
||||
expect(await locator.count()).toEqual(1);
|
||||
}
|
||||
@@ -67,6 +69,7 @@ test.describe("unauthorized user sees correct translations (zh)", async () => {
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
await page.waitForSelector("text=欢迎回来");
|
||||
const locator = page.getByText("欢迎回来", { exact: true });
|
||||
expect(await locator.count()).toEqual(1);
|
||||
}
|
||||
@@ -91,6 +94,7 @@ test.describe("unauthorized user sees correct translations (zh-CN)", async () =>
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
await page.waitForSelector("text=欢迎回来");
|
||||
const locator = page.getByText("欢迎回来", { exact: true });
|
||||
expect(await locator.count()).toEqual(1);
|
||||
}
|
||||
@@ -115,6 +119,7 @@ test.describe("unauthorized user sees correct translations (zh-TW)", async () =>
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
await page.waitForSelector("text=歡迎回來");
|
||||
const locator = page.getByText("歡迎回來", { exact: true });
|
||||
expect(await locator.count()).toEqual(1);
|
||||
}
|
||||
@@ -139,6 +144,7 @@ test.describe("unauthorized user sees correct translations (pt)", async () => {
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
await page.waitForSelector("text=Olá novamente");
|
||||
const locator = page.getByText("Olá novamente", { exact: true });
|
||||
expect(await locator.count()).toEqual(1);
|
||||
}
|
||||
@@ -163,6 +169,7 @@ test.describe("unauthorized user sees correct translations (pt-br)", async () =>
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
await page.waitForSelector("text=Bem-vindo(a) novamente");
|
||||
const locator = page.getByText("Bem-vindo(a) novamente", { exact: true });
|
||||
expect(await locator.count()).toEqual(1);
|
||||
}
|
||||
@@ -188,6 +195,7 @@ test.describe("unauthorized user sees correct translations (es-419)", async () =
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
await page.waitForSelector("text=Bienvenido de nuevo");
|
||||
const locator = page.getByText("Bienvenido de nuevo", { exact: true });
|
||||
expect(await locator.count()).toEqual(1);
|
||||
}
|
||||
|
||||
@@ -653,6 +653,9 @@ test.describe("Out of office", () => {
|
||||
await user.apiLogin();
|
||||
await page.goto("/settings/my-account/out-of-office");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await page.waitForResponse(
|
||||
(response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
|
||||
);
|
||||
await page.locator('[data-testid="add-filter-button"]').click();
|
||||
await page.locator('[data-testid="add-filter-item-dateRange"]').click();
|
||||
await expect(
|
||||
@@ -667,6 +670,9 @@ test.describe("Out of office", () => {
|
||||
await user.apiLogin();
|
||||
await page.goto("/settings/my-account/out-of-office");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await page.waitForResponse(
|
||||
(response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
|
||||
);
|
||||
await page.locator('[data-testid="add-filter-button"]').click();
|
||||
await page.locator('[data-testid="add-filter-item-dateRange"]').click();
|
||||
await page.locator('[data-testid="add-filter-button"]').click();
|
||||
@@ -766,12 +772,10 @@ test.describe("Out of office", () => {
|
||||
await test.step("Default filter - 'Last 7 Days'", async () => {
|
||||
await page.locator('[data-testid="add-filter-button"]').click();
|
||||
await page.locator('[data-testid="add-filter-item-dateRange"]').click();
|
||||
await page.locator('[data-testid="add-filter-button"]').click();
|
||||
const entriesListRespPromise = page.waitForResponse(
|
||||
await page.waitForResponse(
|
||||
(response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
|
||||
);
|
||||
await page.locator('[data-testid="add-filter-button"]').click();
|
||||
await entriesListRespPromise;
|
||||
|
||||
//1 OOO record should be visible for member3, end=currentDate-4days
|
||||
expect(await page.locator('[data-testid^="table-redirect-"]').count()).toBe(1);
|
||||
@@ -783,12 +787,11 @@ test.describe("Out of office", () => {
|
||||
//Select 'Last 30 Days'
|
||||
await test.step("select 'Last 30 Days'", async () => {
|
||||
await page.locator('[data-testid="filter-popover-trigger-dateRange"]').click();
|
||||
const entriesListRespPromise = page.waitForResponse(
|
||||
await page.locator(`[data-testid="date-range-options-t"]`).click();
|
||||
await page.waitForResponse(
|
||||
(response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
|
||||
);
|
||||
await page.locator(`[data-testid="date-range-options-t"]`).click();
|
||||
await page.locator('[data-testid="filter-popover-trigger-dateRange"]').click();
|
||||
await entriesListRespPromise;
|
||||
|
||||
//2 OOO records should be visible end=currentDate-4days, end=currentDate-12days
|
||||
expect(await page.locator('[data-testid^="table-redirect-"]').count()).toBe(2);
|
||||
|
||||
@@ -867,6 +867,7 @@
|
||||
"disabled": "تم التعطيل",
|
||||
"disable": "تعطيل",
|
||||
"billing": "الفوترة",
|
||||
"here": "هنا",
|
||||
"manage_your_billing_info": "قم بإدارة معلومات الفوترة لديك وإلغاء اشتراكك.",
|
||||
"availability": "الأوقات المتاحة",
|
||||
"edit_availability": "تعديل الإتاحة",
|
||||
@@ -1548,6 +1549,7 @@
|
||||
"transcript_from_previous_call": "التفريغ النصي من مكالمة الأخيرة على {{appName}} جاهز للتحميل. الروابط متاحة لمدة ساعة واحدة فقط",
|
||||
"you_can_download_transcript_from_attachments": "يمكنك أيضاً تحميل النص المكتوب من المرفقات",
|
||||
"link_valid_for_12_hrs": "ملاحظة: رابط التحميل متاح لمدة 12 ساعة فقط. يمكنك إنشاء رابط تحميل جديد بإتباع التعليمات <1>هنا</1>.",
|
||||
"link_valid_for_12_hrs_description": "ملاحظة: رابط التنزيل صالح لمدة 12 ساعة فقط. يمكنك إنشاء رابط تنزيل جديد باتباع التعليمات.",
|
||||
"create_your_first_form": "إنشاء أول استمارة",
|
||||
"create_your_first_form_description": "باستخدام نماذج المسارات، يمكنك طرح أسئلة التأهيل والتوجيه إلى الشخص/نوع الحدث المناسب.",
|
||||
"create_your_first_webhook": "إنشاء أول Webhook",
|
||||
@@ -2917,6 +2919,7 @@
|
||||
"dry_run_mode_active": "أنت تقوم بحجز تجريبي.",
|
||||
"icon_showcase": "معرض الأيقونات",
|
||||
"icons_showcase": "معرض الأيقونات",
|
||||
"migrating_from_team": "الانتقال من الفريق",
|
||||
"feature_currently_disabled": "الميزة معطلة حالياً",
|
||||
"choose_an_option": "اختر خيارًا",
|
||||
"enter_option_value": "أدخل قيمة الخيار",
|
||||
@@ -2946,6 +2949,32 @@
|
||||
"asc": "تصاعدي",
|
||||
"desc": "تنازلي",
|
||||
"verify_email_change": "تأكيد تغيير البريد الإلكتروني",
|
||||
"organization_slug_taken": "الاسم المختصر للمؤسسة مستخدم بالفعل",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "لا يمكنك إنشاء مؤسسة لأنك بالفعل جزء من مؤسسة",
|
||||
"you_need_to_complete_user_onboarding_before_creating_an_organization": "تحتاج إلى إكمال عملية تأهيل المستخدم قبل إنشاء مؤسسة",
|
||||
"use_company_email_to_create_an_organization": "استخدم البريد الإلكتروني للشركة لإنشاء مؤسسة",
|
||||
"you_cannot_create_a_platform_organization_as_you_are_already_part_of_a_team": "لا يمكنك إنشاء مؤسسة على المنصة لأنك بالفعل جزء من فريق",
|
||||
"you_need_to_have_minimum_published_teams": "تحتاج إلى الحد الأدنى من الفرق المنشورة",
|
||||
"you_are_part_of_this_organization_already": "أنت بالفعل جزء من هذه المؤسسة",
|
||||
"you_are_part_of_another_organization_cannot_create_organization": "أنت جزء من مؤسسة أخرى، لا يمكنك إنشاء مؤسسة جديدة",
|
||||
"organization_payment_success": "نجاح دفع المؤسسة",
|
||||
"organization_payment_success_description": "تم إنشاء مؤسستك بنجاح",
|
||||
"this_may_take_a_few_seconds": "قد يستغرق هذا بضع ثوانٍ...",
|
||||
"organization_onboarding_status": "حالة تأهيل المؤسسة",
|
||||
"organization_onboarding_status_description": "وصف حالة تأهيل المؤسسة",
|
||||
"payment_failed": "فشل الدفع",
|
||||
"payment_failed_description": "لقد فشلت عملية الدفع الخاصة بك. يرجى المحاولة مرة أخرى.",
|
||||
"creating_your_organization": "جارٍ إنشاء مؤسستك",
|
||||
"organization_onboarding_already_completed": "تم إكمال تأهيل المؤسسة بالفعل",
|
||||
"organization_onboarding_already_exists": "تأهيل المؤسسة موجود بالفعل",
|
||||
"organization_onboarding_not_found": "لم يتم العثور على تأهيل المؤسسة",
|
||||
"payment_successful": "تم الدفع بنجاح",
|
||||
"handover_onboarding_page_title": "تسليم عملية التأهيل",
|
||||
"handover_onboarding_page_description": "تسليم عملية التأهيل للمستخدم",
|
||||
"organization_already_exists_with_this_slug": "المؤسسة موجودة بالفعل بهذا الاسم المختصر",
|
||||
"you_do_not_have_permission_to_create_an_organization_for_this_email": "ليس لديك الصلاحية لإنشاء مؤسسة لهذا البريد الإلكتروني",
|
||||
"you_do_not_have_permission_to_migrate_one_or_more_of_the_teams": "ليس لديك الصلاحية لنقل فريق واحد أو أكثر",
|
||||
"organization_has_subscription_already": "المؤسسة لديها اشتراك بالفعل",
|
||||
"rr_reset_interval": "فترة إعادة تعيين الترتيب الدوري",
|
||||
"interval": "الفترة",
|
||||
"rr_reset_interval_description": "تحدد مدى تكرار إعادة تعيين عدد الحجوزات في الترتيب الدوري لضمان توزيع متوازن.",
|
||||
|
||||
@@ -867,6 +867,7 @@
|
||||
"disabled": "Deaktiv",
|
||||
"disable": "Deaktiv et",
|
||||
"billing": "Ödəniş",
|
||||
"here": "burada",
|
||||
"manage_your_billing_info": "Ödəniş məlumatlarınızı idarə edin və abunəliyinizi ləğv edin.",
|
||||
"availability": "Mövcudluq",
|
||||
"edit_availability": "Mövcudluğu redaktə et",
|
||||
@@ -1548,6 +1549,7 @@
|
||||
"transcript_from_previous_call": "{{appName}} üzərindən son zənginizin transkripti yükləməyə hazırdır. Link yalnız 1 saat etibarlıdır",
|
||||
"you_can_download_transcript_from_attachments": "Siz həmçinin transkripti əlavələrdən yükləyə bilərsiniz",
|
||||
"link_valid_for_12_hrs": "Qeyd: Yükləmə linki yalnız 12 saat etibarlıdır. Yeni yükləmə linki yaratmaq üçün təlimatları izləyərək <1>buradan</1> əldə edə bilərsiniz.",
|
||||
"link_valid_for_12_hrs_description": "Qeyd: Yükləmə linki yalnız 12 saat ərzində etibarlıdır. Təlimatlara əməl edərək yeni yükləmə linki yarada bilərsiniz.",
|
||||
"create_your_first_form": "İlk formanızı yaradın",
|
||||
"create_your_first_form_description": "Yönləndirmə formaları ilə uyğun suallar verə və doğru şəxsə və ya tədbir növünə yönləndirə bilərsiniz.",
|
||||
"create_your_first_webhook": "İlk Webhook-unuzu yaradın",
|
||||
@@ -2917,6 +2919,7 @@
|
||||
"dry_run_mode_active": "Siz test rezervasiyası edirsiniz.",
|
||||
"icon_showcase": "İkon nümayişi",
|
||||
"icons_showcase": "İkonların nümayişi",
|
||||
"migrating_from_team": "Komandadan köçürülür",
|
||||
"feature_currently_disabled": "Funksiya hazırda deaktivdir",
|
||||
"choose_an_option": "Seçim edin",
|
||||
"enter_option_value": "Seçim dəyərini daxil edin",
|
||||
@@ -2946,6 +2949,32 @@
|
||||
"asc": "Artan",
|
||||
"desc": "Azalan",
|
||||
"verify_email_change": "E-poçt dəyişikliyini təsdiqlə",
|
||||
"organization_slug_taken": "Təşkilat slaqı artıq istifadə olunub",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Siz artıq bir təşkilatın üzvü olduğunuz üçün yeni təşkilat yarada bilməzsiniz",
|
||||
"you_need_to_complete_user_onboarding_before_creating_an_organization": "Təşkilat yaratmadan əvvəl istifadəçi qeydiyyatını tamamlamalısınız",
|
||||
"use_company_email_to_create_an_organization": "Təşkilat yaratmaq üçün şirkət e-poçtundan istifadə edin",
|
||||
"you_cannot_create_a_platform_organization_as_you_are_already_part_of_a_team": "Siz artıq bir komandanın üzvü olduğunuz üçün platforma təşkilatı yarada bilməzsiniz",
|
||||
"you_need_to_have_minimum_published_teams": "Minimum sayda dərc edilmiş komandalarınız olmalıdır",
|
||||
"you_are_part_of_this_organization_already": "Siz artıq bu təşkilatın üzvüsünüz",
|
||||
"you_are_part_of_another_organization_cannot_create_organization": "Siz başqa bir təşkilatın üzvüsünüz, yeni təşkilat yarada bilməzsiniz",
|
||||
"organization_payment_success": "Təşkilat ödənişi uğurludur",
|
||||
"organization_payment_success_description": "Təşkilatınız uğurla yaradılıb",
|
||||
"this_may_take_a_few_seconds": "Bu bir neçə saniyə çəkə bilər...",
|
||||
"organization_onboarding_status": "Təşkilat qeydiyyat statusu",
|
||||
"organization_onboarding_status_description": "Təşkilat qeydiyyat statusunun təsviri",
|
||||
"payment_failed": "Ödəniş uğursuz oldu",
|
||||
"payment_failed_description": "Ödənişiniz uğursuz oldu. Zəhmət olmasa yenidən cəhd edin.",
|
||||
"creating_your_organization": "Təşkilatınız yaradılır",
|
||||
"organization_onboarding_already_completed": "Təşkilat qeydiyyatı artıq tamamlanıb",
|
||||
"organization_onboarding_already_exists": "Təşkilat qeydiyyatı artıq mövcuddur",
|
||||
"organization_onboarding_not_found": "Təşkilat qeydiyyatı tapılmadı",
|
||||
"payment_successful": "Ödəniş uğurlu oldu",
|
||||
"handover_onboarding_page_title": "Qeydiyyatın təhvil verilməsi",
|
||||
"handover_onboarding_page_description": "İstifadəçiyə qeydiyyatın təhvil verilməsi",
|
||||
"organization_already_exists_with_this_slug": "Bu slug ilə təşkilat artıq mövcuddur",
|
||||
"you_do_not_have_permission_to_create_an_organization_for_this_email": "Bu e-poçt üçün təşkilat yaratmağa icazəniz yoxdur",
|
||||
"you_do_not_have_permission_to_migrate_one_or_more_of_the_teams": "Bir və ya bir neçə komandanı köçürməyə icazəniz yoxdur",
|
||||
"organization_has_subscription_already": "Təşkilatın artıq abunəliyi var",
|
||||
"rr_reset_interval": "Round Robin Sıfırlama İntervalı",
|
||||
"interval": "İnterval",
|
||||
"rr_reset_interval_description": "Tarazlı paylaşımı təmin etmək üçün round robin rezervasiya sayının nə qədər tez-tez sıfırlanacağını müəyyən edir.",
|
||||
|
||||
@@ -867,6 +867,7 @@
|
||||
"disabled": "Деактивирано",
|
||||
"disable": "Деактивиране",
|
||||
"billing": "Фактуриране",
|
||||
"here": "тук",
|
||||
"manage_your_billing_info": "Управлявайте вашата информация за фактуриране и прекратете абонамента си.",
|
||||
"availability": "Наличност",
|
||||
"edit_availability": "Редактиране на наличност",
|
||||
@@ -1548,6 +1549,7 @@
|
||||
"transcript_from_previous_call": "Транскриптът от последния ви разговор в {{appName}} е готов за изтегляне. Линковете са валидни само за 1 час",
|
||||
"you_can_download_transcript_from_attachments": "Можете също да изтеглите транскрипта от прикачените файлове",
|
||||
"link_valid_for_12_hrs": "Забележка: Линкът за изтегляне е валиден само за 12 часа. Можете да генерирате нов линк за изтегляне, като следвате инструкциите <1>тук</1>.",
|
||||
"link_valid_for_12_hrs_description": "Забележка: Връзката за изтегляне е валидна само за 12 часа. Можете да генерирате нова връзка за изтегляне, като следвате инструкциите.",
|
||||
"create_your_first_form": "Създайте първия си формуляр",
|
||||
"create_your_first_form_description": "С формулярите за маршрутизиране можете да задавате квалификационни въпроси и да пренасочвате към правилния човек или тип събитие.",
|
||||
"create_your_first_webhook": "Създайте първия си уебхук",
|
||||
@@ -2917,6 +2919,7 @@
|
||||
"dry_run_mode_active": "Правите тестова резервация.",
|
||||
"icon_showcase": "Галерия с икони",
|
||||
"icons_showcase": "Галерия с икони",
|
||||
"migrating_from_team": "Мигриране от екип",
|
||||
"feature_currently_disabled": "Функцията е деактивирана в момента",
|
||||
"choose_an_option": "Изберете опция",
|
||||
"enter_option_value": "Въведете стойност на опцията",
|
||||
@@ -2946,6 +2949,32 @@
|
||||
"asc": "Възх",
|
||||
"desc": "Низх",
|
||||
"verify_email_change": "Потвърди промяната на имейл",
|
||||
"organization_slug_taken": "Слъгът на организацията вече е зает",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Не можете да създадете организация, тъй като вече сте част от организация",
|
||||
"you_need_to_complete_user_onboarding_before_creating_an_organization": "Трябва да завършите въвеждането на потребителя, преди да създадете организация",
|
||||
"use_company_email_to_create_an_organization": "Използвайте фирмен имейл, за да създадете организация",
|
||||
"you_cannot_create_a_platform_organization_as_you_are_already_part_of_a_team": "Не можете да създадете платформена организация, тъй като вече сте част от екип",
|
||||
"you_need_to_have_minimum_published_teams": "Трябва да имате минимален брой публикувани екипи",
|
||||
"you_are_part_of_this_organization_already": "Вече сте част от тази организация",
|
||||
"you_are_part_of_another_organization_cannot_create_organization": "Вие сте част от друга организация, не можете да създадете нова организация",
|
||||
"organization_payment_success": "Успешно плащане за организацията",
|
||||
"organization_payment_success_description": "Вашата организация е създадена успешно",
|
||||
"this_may_take_a_few_seconds": "Това може да отнеме няколко секунди...",
|
||||
"organization_onboarding_status": "Статус на въвеждане в организацията",
|
||||
"organization_onboarding_status_description": "Описание на статуса на въвеждане в организацията",
|
||||
"payment_failed": "Плащането не бе успешно",
|
||||
"payment_failed_description": "Вашето плащане не бе успешно. Моля, опитайте отново.",
|
||||
"creating_your_organization": "Създаване на вашата организация",
|
||||
"organization_onboarding_already_completed": "Въвеждането в организацията вече е завършено",
|
||||
"organization_onboarding_already_exists": "Въвеждането в организацията вече съществува",
|
||||
"organization_onboarding_not_found": "Въвеждането в организацията не е намерено",
|
||||
"payment_successful": "Успешно плащане",
|
||||
"handover_onboarding_page_title": "Предаване на въвеждането",
|
||||
"handover_onboarding_page_description": "Предаване на въвеждането на потребителя",
|
||||
"organization_already_exists_with_this_slug": "Вече съществува организация с този слъг",
|
||||
"you_do_not_have_permission_to_create_an_organization_for_this_email": "Нямате разрешение да създадете организация за този имейл",
|
||||
"you_do_not_have_permission_to_migrate_one_or_more_of_the_teams": "Нямате разрешение да мигрирате един или повече от екипите",
|
||||
"organization_has_subscription_already": "Организацията вече има абонамент",
|
||||
"rr_reset_interval": "Интервал за нулиране на Round Robin",
|
||||
"interval": "Интервал",
|
||||
"rr_reset_interval_description": "Определя колко често се нулира броят на резервациите в round robin, за да се осигури балансирано разпределение.",
|
||||
|
||||
@@ -867,6 +867,7 @@
|
||||
"disabled": "Desactivat",
|
||||
"disable": "Desactivar",
|
||||
"billing": "Facturació",
|
||||
"here": "aquí",
|
||||
"manage_your_billing_info": "Gestiona la teva informació de facturació i cancel·la la teva subscripció.",
|
||||
"availability": "Disponibilitat",
|
||||
"edit_availability": "Editar disponibilitat",
|
||||
@@ -1548,6 +1549,7 @@
|
||||
"transcript_from_previous_call": "La transcripció de la teva trucada recent a {{appName}} està llesta per descarregar. Els enllaços només són vàlids durant 1 hora",
|
||||
"you_can_download_transcript_from_attachments": "També podeu descarregar la transcripció des dels arxius adjunts",
|
||||
"link_valid_for_12_hrs": "Nota: L'enllaç de descàrrega només és vàlid durant 12 hores. Pots generar un nou enllaç seguint les instruccions <1>aquí</1>.",
|
||||
"link_valid_for_12_hrs_description": "Nota: L'enllaç de descàrrega només és vàlid durant 12 hores. Podeu generar un nou enllaç de descàrrega seguint les instruccions.",
|
||||
"create_your_first_form": "Crea el teu primer formulari",
|
||||
"create_your_first_form_description": "Amb els Formularis de Ruta pots fer preguntes de qualificació i redirigir a la persona o tipus d'esdeveniment correcte.",
|
||||
"create_your_first_webhook": "Crea el teu primer Webhook",
|
||||
@@ -2917,6 +2919,7 @@
|
||||
"dry_run_mode_active": "Estàs fent una reserva de prova.",
|
||||
"icon_showcase": "Mostra d'icones",
|
||||
"icons_showcase": "Mostra d'icones",
|
||||
"migrating_from_team": "Migrant des de l'equip",
|
||||
"feature_currently_disabled": "La funcionalitat està actualment desactivada",
|
||||
"choose_an_option": "Tria una opció",
|
||||
"enter_option_value": "Introdueix el valor de l'opció",
|
||||
@@ -2946,6 +2949,32 @@
|
||||
"asc": "Asc",
|
||||
"desc": "Desc",
|
||||
"verify_email_change": "Verifica el canvi de correu electrònic",
|
||||
"organization_slug_taken": "L'slug de l'organització ja està en ús",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "No podeu crear una organització perquè ja formeu part d'una",
|
||||
"you_need_to_complete_user_onboarding_before_creating_an_organization": "Heu de completar la incorporació d'usuari abans de crear una organització",
|
||||
"use_company_email_to_create_an_organization": "Utilitzeu el correu electrònic de l'empresa per crear una organització",
|
||||
"you_cannot_create_a_platform_organization_as_you_are_already_part_of_a_team": "No podeu crear una organització de plataforma perquè ja formeu part d'un equip",
|
||||
"you_need_to_have_minimum_published_teams": "Necessiteu tenir un mínim d'equips publicats",
|
||||
"you_are_part_of_this_organization_already": "Ja formeu part d'aquesta organització",
|
||||
"you_are_part_of_another_organization_cannot_create_organization": "Formeu part d'una altra organització, no podeu crear-ne una de nova",
|
||||
"organization_payment_success": "Pagament de l'organització realitzat amb èxit",
|
||||
"organization_payment_success_description": "La vostra organització s'ha creat correctament",
|
||||
"this_may_take_a_few_seconds": "Això pot trigar uns segons...",
|
||||
"organization_onboarding_status": "Estat d'incorporació de l'organització",
|
||||
"organization_onboarding_status_description": "Descripció de l'estat d'incorporació de l'organització",
|
||||
"payment_failed": "El pagament ha fallat",
|
||||
"payment_failed_description": "El vostre pagament ha fallat. Si us plau, torneu-ho a provar.",
|
||||
"creating_your_organization": "Creant la vostra organització",
|
||||
"organization_onboarding_already_completed": "La incorporació de l'organització ja s'ha completat",
|
||||
"organization_onboarding_already_exists": "La incorporació de l'organització ja existeix",
|
||||
"organization_onboarding_not_found": "No s'ha trobat la incorporació de l'organització",
|
||||
"payment_successful": "Pagament realitzat amb èxit",
|
||||
"handover_onboarding_page_title": "Traspàs d'incorporació",
|
||||
"handover_onboarding_page_description": "Traspàs d'incorporació a l'usuari",
|
||||
"organization_already_exists_with_this_slug": "Ja existeix una organització amb aquest slug",
|
||||
"you_do_not_have_permission_to_create_an_organization_for_this_email": "No teniu permís per crear una organització per a aquest correu electrònic",
|
||||
"you_do_not_have_permission_to_migrate_one_or_more_of_the_teams": "No teniu permís per migrar un o més dels equips",
|
||||
"organization_has_subscription_already": "L'organització ja té una subscripció",
|
||||
"rr_reset_interval": "Interval de reinici del Round Robin",
|
||||
"interval": "Interval",
|
||||
"rr_reset_interval_description": "Determina amb quina freqüència es reinicia el comptador de reserves del round robin per assegurar una distribució equilibrada.",
|
||||
|
||||
@@ -867,6 +867,7 @@
|
||||
"disabled": "Vypnuto",
|
||||
"disable": "Vypnout",
|
||||
"billing": "Fakturace",
|
||||
"here": "zde",
|
||||
"manage_your_billing_info": "Spravujte své fakturační údaje a zrušte své předplatné.",
|
||||
"availability": "Dostupnost",
|
||||
"edit_availability": "Upravit dostupnost",
|
||||
@@ -1548,6 +1549,7 @@
|
||||
"transcript_from_previous_call": "Přepis z vašeho nedávného hovoru na {{appName}} je připraven ke stažení. Odkazy jsou platné pouze 1 hodinu",
|
||||
"you_can_download_transcript_from_attachments": "Přepis si můžete také stáhnout z příloh",
|
||||
"link_valid_for_12_hrs": "Poznámka: Odkaz ke stažení je platný pouze 12 hodin. Nový odkaz ke stažení můžete vygenerovat podle pokynů <1>zde</1>.",
|
||||
"link_valid_for_12_hrs_description": "Poznámka: Odkaz ke stažení je platný pouze 12 hodin. Nový odkaz ke stažení můžete vygenerovat podle pokynů.",
|
||||
"create_your_first_form": "Vytvořte svůj první formulář",
|
||||
"create_your_first_form_description": "Pomocí adresných formulářů můžete klást kvalifikační otázky a adresovat je správné osobě nebo typu události.",
|
||||
"create_your_first_webhook": "Vytvořte svůj první webhook",
|
||||
@@ -2917,6 +2919,7 @@
|
||||
"dry_run_mode_active": "Provádíte testovací rezervaci.",
|
||||
"icon_showcase": "Ukázka ikon",
|
||||
"icons_showcase": "Ukázka ikon",
|
||||
"migrating_from_team": "Migrace z týmu",
|
||||
"feature_currently_disabled": "Funkce je momentálně vypnutá",
|
||||
"choose_an_option": "Vyberte možnost",
|
||||
"enter_option_value": "Zadejte hodnotu možnosti",
|
||||
@@ -2946,6 +2949,32 @@
|
||||
"asc": "Vzestupně",
|
||||
"desc": "Sestupně",
|
||||
"verify_email_change": "Ověřit změnu e-mailu",
|
||||
"organization_slug_taken": "Slug organizace je již obsazen",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Nemůžete vytvořit organizaci, protože již jste součástí jiné organizace",
|
||||
"you_need_to_complete_user_onboarding_before_creating_an_organization": "Před vytvořením organizace musíte dokončit úvodní zaškolení uživatele",
|
||||
"use_company_email_to_create_an_organization": "Pro vytvoření organizace použijte firemní e-mail",
|
||||
"you_cannot_create_a_platform_organization_as_you_are_already_part_of_a_team": "Nemůžete vytvořit platformní organizaci, protože již jste součástí týmu",
|
||||
"you_need_to_have_minimum_published_teams": "Musíte mít minimální počet publikovaných týmů",
|
||||
"you_are_part_of_this_organization_already": "Již jste součástí této organizace",
|
||||
"you_are_part_of_another_organization_cannot_create_organization": "Jste součástí jiné organizace, nelze vytvořit novou organizaci",
|
||||
"organization_payment_success": "Platba za organizaci byla úspěšná",
|
||||
"organization_payment_success_description": "Vaše organizace byla úspěšně vytvořena",
|
||||
"this_may_take_a_few_seconds": "Toto může trvat několik sekund...",
|
||||
"organization_onboarding_status": "Stav úvodního zaškolení organizace",
|
||||
"organization_onboarding_status_description": "Popis stavu úvodního zaškolení organizace",
|
||||
"payment_failed": "Platba se nezdařila",
|
||||
"payment_failed_description": "Vaše platba se nezdařila. Zkuste to prosím znovu.",
|
||||
"creating_your_organization": "Vytváření vaší organizace",
|
||||
"organization_onboarding_already_completed": "Úvodní zaškolení organizace již bylo dokončeno",
|
||||
"organization_onboarding_already_exists": "Úvodní zaškolení organizace již existuje",
|
||||
"organization_onboarding_not_found": "Úvodní zaškolení organizace nebylo nalezeno",
|
||||
"payment_successful": "Platba byla úspěšná",
|
||||
"handover_onboarding_page_title": "Předání úvodního zaškolení",
|
||||
"handover_onboarding_page_description": "Předání úvodního zaškolení uživateli",
|
||||
"organization_already_exists_with_this_slug": "Organizace s tímto slugem již existuje",
|
||||
"you_do_not_have_permission_to_create_an_organization_for_this_email": "Nemáte oprávnění vytvořit organizaci pro tento e-mail",
|
||||
"you_do_not_have_permission_to_migrate_one_or_more_of_the_teams": "Nemáte oprávnění migrovat jeden nebo více týmů",
|
||||
"organization_has_subscription_already": "Organizace již má předplatné",
|
||||
"rr_reset_interval": "Interval resetování Round Robin",
|
||||
"interval": "Interval",
|
||||
"rr_reset_interval_description": "Určuje, jak často se resetuje počet rezervací v systému round robin pro zajištění vyváženého rozdělení.",
|
||||
|
||||
@@ -867,6 +867,7 @@
|
||||
"disabled": "Deaktiveret",
|
||||
"disable": "Deaktivér",
|
||||
"billing": "Fakturering",
|
||||
"here": "her",
|
||||
"manage_your_billing_info": "Administrer dine faktureringsoplysninger og annuller dit abonnement.",
|
||||
"availability": "Tilgængelighed",
|
||||
"edit_availability": "Redigér tilgængelighed",
|
||||
@@ -1548,6 +1549,7 @@
|
||||
"transcript_from_previous_call": "Udskrift fra dit seneste opkald på {{appName}} er klar til download. Links er kun gyldige i 1 time",
|
||||
"you_can_download_transcript_from_attachments": "Du kan også downloade udskriften fra vedhæftede filer",
|
||||
"link_valid_for_12_hrs": "Bemærk: Downloadlinket er kun gyldigt i 12 timer. Du kan generere et nyt downloadlink ved at følge instruktionerne <1>her</1>.",
|
||||
"link_valid_for_12_hrs_description": "Bemærk: Downloadlinket er kun gyldigt i 12 timer. Du kan generere et nyt downloadlink ved at følge instruktionerne.",
|
||||
"create_your_first_form": "Opret din første formular",
|
||||
"create_your_first_form_description": "Med Routing Forms kan du stille kvalificerende spørgsmål og rute til den korrekte person eller begivenhedstype.",
|
||||
"create_your_first_webhook": "Opret din første Webhook",
|
||||
@@ -2917,6 +2919,7 @@
|
||||
"dry_run_mode_active": "Du foretager en testbooking.",
|
||||
"icon_showcase": "Ikonoversigt",
|
||||
"icons_showcase": "Ikonoversigt",
|
||||
"migrating_from_team": "Migrerer fra team",
|
||||
"feature_currently_disabled": "Funktionen er i øjeblikket deaktiveret",
|
||||
"choose_an_option": "Vælg en mulighed",
|
||||
"enter_option_value": "Indtast værdi for mulighed",
|
||||
@@ -2946,6 +2949,32 @@
|
||||
"asc": "Stigende",
|
||||
"desc": "Faldende",
|
||||
"verify_email_change": "Bekræft ændring af e-mail",
|
||||
"organization_slug_taken": "Organisationens slug er allerede taget",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Du kan ikke oprette en organisation, da du allerede er en del af en organisation",
|
||||
"you_need_to_complete_user_onboarding_before_creating_an_organization": "Du skal gennemføre brugeronboarding, før du opretter en organisation",
|
||||
"use_company_email_to_create_an_organization": "Brug firmamail til at oprette en organisation",
|
||||
"you_cannot_create_a_platform_organization_as_you_are_already_part_of_a_team": "Du kan ikke oprette en platformsorganisation, da du allerede er en del af et team",
|
||||
"you_need_to_have_minimum_published_teams": "Du skal have et minimum af offentliggjorte teams",
|
||||
"you_are_part_of_this_organization_already": "Du er allerede en del af denne organisation",
|
||||
"you_are_part_of_another_organization_cannot_create_organization": "Du er en del af en anden organisation, kan ikke oprette en ny organisation",
|
||||
"organization_payment_success": "Organisationsbetaling gennemført",
|
||||
"organization_payment_success_description": "Din organisation er blevet oprettet med succes",
|
||||
"this_may_take_a_few_seconds": "Dette kan tage et par sekunder...",
|
||||
"organization_onboarding_status": "Status for organisationens onboarding",
|
||||
"organization_onboarding_status_description": "Beskrivelse af status for organisationens onboarding",
|
||||
"payment_failed": "Betaling mislykkedes",
|
||||
"payment_failed_description": "Din betaling er mislykkedes. Prøv venligst igen.",
|
||||
"creating_your_organization": "Opretter din organisation",
|
||||
"organization_onboarding_already_completed": "Organisationens onboarding er allerede gennemført",
|
||||
"organization_onboarding_already_exists": "Organisationens onboarding eksisterer allerede",
|
||||
"organization_onboarding_not_found": "Organisationens onboarding blev ikke fundet",
|
||||
"payment_successful": "Betaling gennemført",
|
||||
"handover_onboarding_page_title": "Overdragelse af onboarding",
|
||||
"handover_onboarding_page_description": "Overdragelse af onboarding til brugeren",
|
||||
"organization_already_exists_with_this_slug": "En organisation med dette slug findes allerede",
|
||||
"you_do_not_have_permission_to_create_an_organization_for_this_email": "Du har ikke tilladelse til at oprette en organisation for denne e-mail",
|
||||
"you_do_not_have_permission_to_migrate_one_or_more_of_the_teams": "Du har ikke tilladelse til at migrere et eller flere af teamene",
|
||||
"organization_has_subscription_already": "Organisationen har allerede et abonnement",
|
||||
"rr_reset_interval": "Round Robin nulstillingsinterval",
|
||||
"interval": "Interval",
|
||||
"rr_reset_interval_description": "Bestemmer hvor ofte round robin-bookingtællingen nulstilles for at sikre en balanceret fordeling.",
|
||||
|
||||
@@ -867,6 +867,7 @@
|
||||
"disabled": "Deaktiviert",
|
||||
"disable": "Deaktivieren",
|
||||
"billing": "Abrechnung",
|
||||
"here": "hier",
|
||||
"manage_your_billing_info": "Verwalten Sie Ihre Rechnungsinformationen und kündigen Sie Ihr Abonnement.",
|
||||
"availability": "Verfügbarkeit",
|
||||
"edit_availability": "Verfügbarkeit bearbeiten",
|
||||
@@ -1117,7 +1118,7 @@
|
||||
"duplicate": "Duplizieren",
|
||||
"offer_seats": "Plätze anbieten",
|
||||
"offer_seats_description": "Bieten Sie Plätze für Buchungen an (dies deaktiviert Gäste & Opt-in-Buchungen)",
|
||||
"seats_available_other": "Verfügbare Plätze",
|
||||
"seats_available_other": "Plätze verfügbar",
|
||||
"seats_nearly_full": "Plätze fast voll",
|
||||
"seats_half_full": "Plätze werden schnell belegt",
|
||||
"number_of_seats": "Anzahl der Plätze pro Termin",
|
||||
@@ -1548,6 +1549,7 @@
|
||||
"transcript_from_previous_call": "Transkript von Ihrem letzten Anruf bei {{appName}} ist zum Herunterladen bereit. Links sind nur für 1 Stunde gültig",
|
||||
"you_can_download_transcript_from_attachments": "Sie können das Protokoll auch aus den Anhängen herunterladen",
|
||||
"link_valid_for_12_hrs": "Hinweis: Der Download-Link ist nur für 12 Stunden gültig. Sie können einen neuen Downloadlink durch das Befolgen der Anweisungen <1>hier</1> generieren.",
|
||||
"link_valid_for_12_hrs_description": "Hinweis: Der Download-Link ist nur 12 Stunden gültig. Sie können einen neuen Download-Link generieren, indem Sie den Anweisungen folgen.",
|
||||
"create_your_first_form": "Erstellen Sie Ihr erstes Formular",
|
||||
"create_your_first_form_description": "Mithilfe von Leitungsformularen können Sie qualifizierende Fragen stellen und zur richtigen Person oder zum richtigen Veranstaltungstyp weiterleiten.",
|
||||
"create_your_first_webhook": "Erstellen Sie Ihren ersten Webhook",
|
||||
@@ -2917,6 +2919,7 @@
|
||||
"dry_run_mode_active": "Sie führen eine Testbuchung durch.",
|
||||
"icon_showcase": "Icon-Übersicht",
|
||||
"icons_showcase": "Icons-Übersicht",
|
||||
"migrating_from_team": "Migration vom Team",
|
||||
"feature_currently_disabled": "Funktion ist derzeit deaktiviert",
|
||||
"choose_an_option": "Option auswählen",
|
||||
"enter_option_value": "Optionswert eingeben",
|
||||
@@ -2946,6 +2949,32 @@
|
||||
"asc": "Aufsteigend",
|
||||
"desc": "Absteigend",
|
||||
"verify_email_change": "E-Mail-Änderung bestätigen",
|
||||
"organization_slug_taken": "Der Organisations-Slug ist bereits vergeben",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Sie können keine Organisation erstellen, da Sie bereits Teil einer Organisation sind",
|
||||
"you_need_to_complete_user_onboarding_before_creating_an_organization": "Sie müssen das Benutzer-Onboarding abschließen, bevor Sie eine Organisation erstellen können",
|
||||
"use_company_email_to_create_an_organization": "Verwenden Sie die Firmen-E-Mail-Adresse, um eine Organisation zu erstellen",
|
||||
"you_cannot_create_a_platform_organization_as_you_are_already_part_of_a_team": "Sie können keine Plattform-Organisation erstellen, da Sie bereits Teil eines Teams sind",
|
||||
"you_need_to_have_minimum_published_teams": "Sie müssen mindestens veröffentlichte Teams haben",
|
||||
"you_are_part_of_this_organization_already": "Sie sind bereits Teil dieser Organisation",
|
||||
"you_are_part_of_another_organization_cannot_create_organization": "Sie sind Teil einer anderen Organisation und können keine neue Organisation erstellen",
|
||||
"organization_payment_success": "Organisationszahlung erfolgreich",
|
||||
"organization_payment_success_description": "Ihre Organisation wurde erfolgreich erstellt",
|
||||
"this_may_take_a_few_seconds": "Dies kann einige Sekunden dauern...",
|
||||
"organization_onboarding_status": "Status des Organisations-Onboardings",
|
||||
"organization_onboarding_status_description": "Beschreibung des Organisations-Onboarding-Status",
|
||||
"payment_failed": "Zahlung fehlgeschlagen",
|
||||
"payment_failed_description": "Ihre Zahlung ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||
"creating_your_organization": "Ihre Organisation wird erstellt",
|
||||
"organization_onboarding_already_completed": "Organisations-Onboarding bereits abgeschlossen",
|
||||
"organization_onboarding_already_exists": "Organisations-Onboarding existiert bereits",
|
||||
"organization_onboarding_not_found": "Organisations-Onboarding nicht gefunden",
|
||||
"payment_successful": "Zahlung erfolgreich",
|
||||
"handover_onboarding_page_title": "Übergabe-Onboarding",
|
||||
"handover_onboarding_page_description": "Übergabe des Onboardings an den Benutzer",
|
||||
"organization_already_exists_with_this_slug": "Eine organisation mit diesem slug existiert bereits",
|
||||
"you_do_not_have_permission_to_create_an_organization_for_this_email": "Sie haben keine berechtigung, eine organisation für diese e-mail-adresse zu erstellen",
|
||||
"you_do_not_have_permission_to_migrate_one_or_more_of_the_teams": "Sie haben keine berechtigung, eines oder mehrere der teams zu migrieren",
|
||||
"organization_has_subscription_already": "Die organisation hat bereits ein abonnement",
|
||||
"rr_reset_interval": "Round Robin Zurücksetzungsintervall",
|
||||
"interval": "Intervall",
|
||||
"rr_reset_interval_description": "Legt fest, wie oft der Round Robin Buchungszähler zurückgesetzt wird, um eine ausgewogene Verteilung zu gewährleisten.",
|
||||
|
||||
@@ -867,6 +867,7 @@
|
||||
"disabled": "Απενεργοποιημένο",
|
||||
"disable": "Απενεργοποίηση",
|
||||
"billing": "Χρεώσεις",
|
||||
"here": "εδώ",
|
||||
"manage_your_billing_info": "Διαχειριστείτε τις πληροφορίες χρέωσης και ακυρώστε τη συνδρομή σας.",
|
||||
"availability": "Διαθεσιμότητα",
|
||||
"edit_availability": "Επεξεργασία διαθεσιμότητας",
|
||||
@@ -1548,6 +1549,7 @@
|
||||
"transcript_from_previous_call": "Η απομαγνητοφώνηση από την πρόσφατη κλήση σας στο {{appName}} είναι έτοιμη για λήψη. Οι σύνδεσμοι ισχύουν μόνο για 1 ώρα",
|
||||
"you_can_download_transcript_from_attachments": "Μπορείτε επίσης να κατεβάσετε το αντίγραφο από τα συνημμένα",
|
||||
"link_valid_for_12_hrs": "Σημείωση: Ο σύνδεσμος λήψης ισχύει μόνο για 12 ώρες. Μπορείτε να δημιουργήσετε νέο σύνδεσμο λήψης ακολουθώντας τις οδηγίες <1>εδώ</1>.",
|
||||
"link_valid_for_12_hrs_description": "Σημείωση: Ο σύνδεσμος λήψης ισχύει μόνο για 12 ώρες. Μπορείτε να δημιουργήσετε νέο σύνδεσμο λήψης ακολουθώντας τις οδηγίες.",
|
||||
"create_your_first_form": "Δημιουργήστε την πρώτη σας φόρμα",
|
||||
"create_your_first_form_description": "Με τις φόρμες δρομολόγησης μπορείτε να θέσετε ερωτήσεις αξιολόγησης και να κατευθύνετε στο σωστό άτομο ή τύπο εκδήλωσης.",
|
||||
"create_your_first_webhook": "Δημιουργήστε το πρώτο σας Webhook",
|
||||
@@ -2917,6 +2919,7 @@
|
||||
"dry_run_mode_active": "Κάνετε μια δοκιμαστική κράτηση.",
|
||||
"icon_showcase": "Παρουσίαση εικονιδίων",
|
||||
"icons_showcase": "Παρουσίαση εικονιδίων",
|
||||
"migrating_from_team": "Μετάβαση από ομάδα",
|
||||
"feature_currently_disabled": "Η λειτουργία είναι απενεργοποιημένη",
|
||||
"choose_an_option": "Επιλέξτε μια επιλογή",
|
||||
"enter_option_value": "Εισαγάγετε την τιμή της επιλογής",
|
||||
@@ -2946,6 +2949,32 @@
|
||||
"asc": "Αύξουσα",
|
||||
"desc": "Φθίνουσα",
|
||||
"verify_email_change": "Επαλήθευση αλλαγής email",
|
||||
"organization_slug_taken": "Το αναγνωριστικό του οργανισμού χρησιμοποιείται ήδη",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Δεν μπορείτε να δημιουργήσετε έναν οργανισμό καθώς είστε ήδη μέλος ενός οργανισμού",
|
||||
"you_need_to_complete_user_onboarding_before_creating_an_organization": "Πρέπει να ολοκληρώσετε την εισαγωγή χρήστη πριν δημιουργήσετε έναν οργανισμό",
|
||||
"use_company_email_to_create_an_organization": "Χρησιμοποιήστε εταιρικό email για να δημιουργήσετε έναν οργανισμό",
|
||||
"you_cannot_create_a_platform_organization_as_you_are_already_part_of_a_team": "Δεν μπορείτε να δημιουργήσετε έναν οργανισμό πλατφόρμας καθώς είστε ήδη μέλος μιας ομάδας",
|
||||
"you_need_to_have_minimum_published_teams": "Πρέπει να έχετε τον ελάχιστο αριθμό δημοσιευμένων ομάδων",
|
||||
"you_are_part_of_this_organization_already": "Είστε ήδη μέλος αυτού του οργανισμού",
|
||||
"you_are_part_of_another_organization_cannot_create_organization": "Είστε μέλος άλλου οργανισμού, δεν μπορείτε να δημιουργήσετε νέο οργανισμό",
|
||||
"organization_payment_success": "Επιτυχής πληρωμή οργανισμού",
|
||||
"organization_payment_success_description": "Ο οργανισμός σας δημιουργήθηκε με επιτυχία",
|
||||
"this_may_take_a_few_seconds": "Αυτό μπορεί να διαρκέσει μερικά δευτερόλεπτα...",
|
||||
"organization_onboarding_status": "Κατάσταση εισαγωγής οργανισμού",
|
||||
"organization_onboarding_status_description": "Περιγραφή κατάστασης εισαγωγής οργανισμού",
|
||||
"payment_failed": "Η πληρωμή απέτυχε",
|
||||
"payment_failed_description": "Η πληρωμή σας απέτυχε. Παρακαλώ δοκιμάστε ξανά.",
|
||||
"creating_your_organization": "Δημιουργία του οργανισμού σας",
|
||||
"organization_onboarding_already_completed": "Η εισαγωγή οργανισμού έχει ήδη ολοκληρωθεί",
|
||||
"organization_onboarding_already_exists": "Η εισαγωγή οργανισμού υπάρχει ήδη",
|
||||
"organization_onboarding_not_found": "Η εισαγωγή οργανισμού δεν βρέθηκε",
|
||||
"payment_successful": "Επιτυχής πληρωμή",
|
||||
"handover_onboarding_page_title": "Παράδοση εισαγωγής",
|
||||
"handover_onboarding_page_description": "Παράδοση εισαγωγής στον χρήστη",
|
||||
"organization_already_exists_with_this_slug": "Υπάρχει ήδη οργανισμός με αυτό το slug",
|
||||
"you_do_not_have_permission_to_create_an_organization_for_this_email": "Δεν έχετε άδεια να δημιουργήσετε οργανισμό για αυτό το email",
|
||||
"you_do_not_have_permission_to_migrate_one_or_more_of_the_teams": "Δεν έχετε άδεια να μεταφέρετε μία ή περισσότερες από τις ομάδες",
|
||||
"organization_has_subscription_already": "Ο οργανισμός έχει ήδη συνδρομή",
|
||||
"rr_reset_interval": "Διάστημα επαναφοράς Round Robin",
|
||||
"interval": "Διάστημα",
|
||||
"rr_reset_interval_description": "Καθορίζει πόσο συχνά επαναφέρεται ο μετρητής κρατήσεων round robin για να διασφαλιστεί η ισορροπημένη κατανομή.",
|
||||
|
||||
@@ -1118,7 +1118,8 @@
|
||||
"duplicate": "Duplicate",
|
||||
"offer_seats": "Offer seats",
|
||||
"offer_seats_description": "Offer seats for booking. This automatically disables guest & opt-in bookings.",
|
||||
"seats_available_other": "available",
|
||||
"seats_available_one": "Seat available",
|
||||
"seats_available_other": "Seats available",
|
||||
"seats_nearly_full": "Seats almost full",
|
||||
"seats_half_full": "Seats filling fast",
|
||||
"number_of_seats": "Number of seats per booking",
|
||||
|
||||
@@ -867,6 +867,7 @@
|
||||
"disabled": "Deshabilitado",
|
||||
"disable": "Deshabilitar",
|
||||
"billing": "Facturación",
|
||||
"here": "aquí",
|
||||
"manage_your_billing_info": "Gestiona tu información de facturación y cancela tu suscripción.",
|
||||
"availability": "Disponibilidad",
|
||||
"edit_availability": "Editar disponibilidad",
|
||||
@@ -1548,6 +1549,7 @@
|
||||
"transcript_from_previous_call": "La transcripción de tu llamada reciente en {{appName}} está lista para descargar. Los enlaces son válidos solo por 1 hora",
|
||||
"you_can_download_transcript_from_attachments": "También puedes descargar la transcripción desde los archivos adjuntos",
|
||||
"link_valid_for_12_hrs": "Nota: El enlace de descarga es válido solo por 12 horas. Puedes generar un nuevo enlace de descarga siguiendo las instrucciones <1>aquí</1>.",
|
||||
"link_valid_for_12_hrs_description": "Nota: El enlace de descarga es válido solo por 12 horas. Puedes generar un nuevo enlace de descarga siguiendo las instrucciones.",
|
||||
"create_your_first_form": "Crea tu primer formulario",
|
||||
"create_your_first_form_description": "Con los Formularios de Enrutamiento puedes hacer preguntas de calificación y dirigir a la persona o tipo de evento correcto.",
|
||||
"create_your_first_webhook": "Crea tu primer Webhook",
|
||||
@@ -2917,6 +2919,7 @@
|
||||
"dry_run_mode_active": "Estás haciendo una reserva de prueba.",
|
||||
"icon_showcase": "Muestra de íconos",
|
||||
"icons_showcase": "Muestra de íconos",
|
||||
"migrating_from_team": "Migrando desde el equipo",
|
||||
"feature_currently_disabled": "La función está desactivada actualmente",
|
||||
"choose_an_option": "Elige una opción",
|
||||
"enter_option_value": "Ingresa el valor de la opción",
|
||||
@@ -2946,6 +2949,32 @@
|
||||
"asc": "Asc",
|
||||
"desc": "Desc",
|
||||
"verify_email_change": "Verificar cambio de correo electrónico",
|
||||
"organization_slug_taken": "El slug de la organización ya está en uso",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "No puedes crear una organización ya que ya eres parte de una organización",
|
||||
"you_need_to_complete_user_onboarding_before_creating_an_organization": "Necesitas completar la incorporación de usuario antes de crear una organización",
|
||||
"use_company_email_to_create_an_organization": "Usa el correo electrónico de la empresa para crear una organización",
|
||||
"you_cannot_create_a_platform_organization_as_you_are_already_part_of_a_team": "No puedes crear una organización de plataforma ya que ya eres parte de un equipo",
|
||||
"you_need_to_have_minimum_published_teams": "Necesitas tener un mínimo de equipos publicados",
|
||||
"you_are_part_of_this_organization_already": "Ya eres parte de esta organización",
|
||||
"you_are_part_of_another_organization_cannot_create_organization": "Eres parte de otra organización, no puedes crear una nueva organización",
|
||||
"organization_payment_success": "Pago de la organización exitoso",
|
||||
"organization_payment_success_description": "Tu organización ha sido creada exitosamente",
|
||||
"this_may_take_a_few_seconds": "Esto puede tomar unos segundos...",
|
||||
"organization_onboarding_status": "Estado de incorporación de la organización",
|
||||
"organization_onboarding_status_description": "Descripción del estado de incorporación de la organización",
|
||||
"payment_failed": "El pago falló",
|
||||
"payment_failed_description": "Tu pago ha fallado. Por favor, inténtalo de nuevo.",
|
||||
"creating_your_organization": "Creando tu organización",
|
||||
"organization_onboarding_already_completed": "La incorporación de la organización ya se completó",
|
||||
"organization_onboarding_already_exists": "La incorporación de la organización ya existe",
|
||||
"organization_onboarding_not_found": "No se encontró la incorporación de la organización",
|
||||
"payment_successful": "Pago exitoso",
|
||||
"handover_onboarding_page_title": "Traspaso de incorporación",
|
||||
"handover_onboarding_page_description": "Traspaso de incorporación al usuario",
|
||||
"organization_already_exists_with_this_slug": "Ya existe una organización con este slug",
|
||||
"you_do_not_have_permission_to_create_an_organization_for_this_email": "No tienes permiso para crear una organización con este correo electrónico",
|
||||
"you_do_not_have_permission_to_migrate_one_or_more_of_the_teams": "No tienes permiso para migrar uno o más de los equipos",
|
||||
"organization_has_subscription_already": "La organización ya tiene una suscripción",
|
||||
"rr_reset_interval": "Intervalo de reinicio de Round Robin",
|
||||
"interval": "Intervalo",
|
||||
"rr_reset_interval_description": "Determina con qué frecuencia se reinicia el conteo de reservas de round robin para asegurar una distribución equilibrada.",
|
||||
|
||||
@@ -867,6 +867,7 @@
|
||||
"disabled": "Desactivado",
|
||||
"disable": "Deshabilitar",
|
||||
"billing": "Facturación",
|
||||
"here": "aquí",
|
||||
"manage_your_billing_info": "Gestione su información de facturación y cancele su suscripción.",
|
||||
"availability": "Disponibilidad",
|
||||
"edit_availability": "Editar disponibilidad",
|
||||
@@ -1548,6 +1549,7 @@
|
||||
"transcript_from_previous_call": "La transcripción de su llamada reciente en {{appName}} está lista para descargar. Los enlaces son válidos solo por 1 hora.",
|
||||
"you_can_download_transcript_from_attachments": "También puedes descargar la transcripción desde los archivos adjuntos",
|
||||
"link_valid_for_12_hrs": "Nota: El enlace de descarga es válido solo por 12 horas. Puede generar un nuevo enlace de descarga siguiendo las instrucciones <1>aquí</1>.",
|
||||
"link_valid_for_12_hrs_description": "Nota: El enlace de descarga es válido solo por 12 horas. Puedes generar un nuevo enlace de descarga siguiendo las instrucciones.",
|
||||
"create_your_first_form": "Crea tu primer formulario",
|
||||
"create_your_first_form_description": "Con los formularios de orientación puedes hacer preguntas calificadoras y encaminarte hacia la persona o tipo de evento correctos.",
|
||||
"create_your_first_webhook": "Crea tu primer Webhook",
|
||||
@@ -2917,6 +2919,7 @@
|
||||
"dry_run_mode_active": "Estás haciendo una reserva de prueba.",
|
||||
"icon_showcase": "Muestra de iconos",
|
||||
"icons_showcase": "Muestra de iconos",
|
||||
"migrating_from_team": "Migrando desde el equipo",
|
||||
"feature_currently_disabled": "La función está actualmente desactivada",
|
||||
"choose_an_option": "Elige una opción",
|
||||
"enter_option_value": "Ingresa el valor de la opción",
|
||||
@@ -2946,6 +2949,32 @@
|
||||
"asc": "Asc",
|
||||
"desc": "Desc",
|
||||
"verify_email_change": "Verificar cambio de correo electrónico",
|
||||
"organization_slug_taken": "El slug de la organización ya está en uso",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "No puedes crear una organización ya que ya formas parte de una",
|
||||
"you_need_to_complete_user_onboarding_before_creating_an_organization": "Necesitas completar la incorporación de usuario antes de crear una organización",
|
||||
"use_company_email_to_create_an_organization": "Usa el correo electrónico de la empresa para crear una organización",
|
||||
"you_cannot_create_a_platform_organization_as_you_are_already_part_of_a_team": "No puedes crear una organización de plataforma ya que ya formas parte de un equipo",
|
||||
"you_need_to_have_minimum_published_teams": "Necesitas tener un mínimo de equipos publicados",
|
||||
"you_are_part_of_this_organization_already": "Ya formas parte de esta organización",
|
||||
"you_are_part_of_another_organization_cannot_create_organization": "Formas parte de otra organización, no puedes crear una nueva",
|
||||
"organization_payment_success": "Pago de la organización exitoso",
|
||||
"organization_payment_success_description": "Tu organización se ha creado con éxito",
|
||||
"this_may_take_a_few_seconds": "Esto puede tardar unos segundos...",
|
||||
"organization_onboarding_status": "Estado de incorporación de la organización",
|
||||
"organization_onboarding_status_description": "Descripción del estado de incorporación de la organización",
|
||||
"payment_failed": "El pago ha fallado",
|
||||
"payment_failed_description": "Tu pago ha fallado. Por favor, inténtalo de nuevo.",
|
||||
"creating_your_organization": "Creando tu organización",
|
||||
"organization_onboarding_already_completed": "La incorporación de la organización ya se ha completado",
|
||||
"organization_onboarding_already_exists": "La incorporación de la organización ya existe",
|
||||
"organization_onboarding_not_found": "No se encontró la incorporación de la organización",
|
||||
"payment_successful": "Pago exitoso",
|
||||
"handover_onboarding_page_title": "Traspaso de incorporación",
|
||||
"handover_onboarding_page_description": "Traspaso de incorporación al usuario",
|
||||
"organization_already_exists_with_this_slug": "Ya existe una organización con este slug",
|
||||
"you_do_not_have_permission_to_create_an_organization_for_this_email": "No tienes permiso para crear una organización con este correo electrónico",
|
||||
"you_do_not_have_permission_to_migrate_one_or_more_of_the_teams": "No tienes permiso para migrar uno o más de los equipos",
|
||||
"organization_has_subscription_already": "La organización ya tiene una suscripción",
|
||||
"rr_reset_interval": "Intervalo de reinicio de rotación",
|
||||
"interval": "Intervalo",
|
||||
"rr_reset_interval_description": "Determina con qué frecuencia se reinicia el conteo de reservas de rotación para asegurar una distribución equilibrada.",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user