Compare commits

..
60 Commits
Author SHA1 Message Date
GitHub Actions ca19aa00c1 chore: release v5.1.2
Post release / release (push) Canceled after 0s
2025-03-19 21:59:27 +00:00
Anik Dhabal Babu 467c979aa7 fix: updating the user does not invalidate/refresh the table (#20228) 2025-03-19 20:28:18 +00:00
Hariom Balhara 9f9bf10040 fix: A user exists with that username (#20217) 2025-03-19 14:34:33 +00:00
Lauris Skraucis 8102192987 refactor: v2 booking responses schema (#20030)
* refactor: when rescheduling fallback on attendee if responses invalid

* chore: submit auto generated docs

* fix: slots event type metadata parsing

* fix: booking responses without email with attendee phone

* refactor

* refactor: test

* remove unused import

* refactor

* test: slots variable duration
2025-03-19 16:32:00 +02:00
Pranav PatilandTushar Bhatt 232f9d288c fix: Loading indicator alignment in settings page (#20219)
* Fix: Loading indicator alignment in profile page

* Added animate spin class

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-19 14:31:05 +00:00
Anik Dhabal Babu 7dab100382 fix: unskip confirm step if there are multiple location (#20051) 2025-03-19 12:17:49 +00:00
Lauris Skraucis 91d4f1c219 feat: booker atom confirm and back button styling (#20162)
* feat: submitButtonDisabled Booker atom prop

* feat: confirmStep.confirmButton & backButton booker styles

* refactor: rename submitButtonDisabled to confirmButtonDisabled

* feat: implement this feature for skip confirm step

* refactor: naming
2025-03-19 11:30:48 +00:00
MorganandSomay Chauhan a64e7f9c2a fix: set default managed users calendars (#20208)
* fix: set default managed users calendars

* fix: module imports

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2025-03-19 09:22:28 +00:00
sean-brydonandKeith Williams 3c1297aa72 fix: calcom trpc sessio circle dep (#19893)
* fix trpc session circle dep

* fixes trpc session cirlce dep

* fix relative imports

* fix more imports to use types and not trpc

* fix exports

* Fixed types

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-19 05:50:22 -03:00
Vijay 96f7a01c5f fix: few flaky tests in ooo and bookings-list (#20170)
* fix flaky tests

* chore
2025-03-19 08:32:47 +00:00
Udit TakkarandTushar Bhatt a4a05878ef fix: use correct skeleton loader (#20197)
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-19 08:07:22 +00:00
Keith WilliamsandhbjORbj 352d68ae1c perf: Remove app-store reference from @calcom/ui (#20188)
* perf: Remove app-store reference from @calcom/ui

* Fixing types

* Fixed casing

* fix type check

* refactor

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-19 05:04:02 -03:00
Tushar Bhatt ffd034679c fix: team-update-on-eventTypes-page (#20152) 2025-03-19 07:28:18 +00:00
Morgan d64ae3c901 fix: missing booking reminder reference in cron (#20200) 2025-03-19 07:27:52 +00:00
Hariom Balhara 7250e22257 fix salesforce query (#20196) 2025-03-19 09:23:12 +02:00
Udit Takkar f459e5d9c4 fix: use correct logo (#20194) 2025-03-19 06:29:07 +00:00
Tushar Bhatt 30bb0f1383 fix: logo-sync-issue (#20190) 2025-03-19 04:44:55 +00:00
Abdullah Arif 24c993a3d2 fix(createButton): add scrolling to empty webhook dropdown (#20191)
Added a scroll-bar class and set a max height on DropdownMenuContent to ensure large dropdowns are scrollable, improving the user experience by making all items accessible without expanding beyond the viewport.
2025-03-19 09:54:09 +05:30
GitHub Actions aa4276be5b chore: release v5.1.1
Post release / release (push) Canceled after 0s
2025-03-18 20:45:23 +00:00
Somay ChauhanandKeith Williams 413aba6e52 fix: add documentation to the possible values of :app in conferencing controller (#20164)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-18 17:41:35 -03:00
sean-brydon c2547acb3d fix eslint error (#20127) 2025-03-18 20:21:08 +00:00
Benny Joo cc1b6bac4a perf: Remove QueryCell from availability view (#20176) 2025-03-18 17:05:02 -03:00
Udit Takkar d5d90a8b87 fix: sms formatting (#18647) 2025-03-18 16:59:13 -03:00
Joe Au-YeungandKeith Williams 40333c81c5 chore: Add logging to Salesforce (#20167)
* Remove unused param

* Add log when no contacts are created

* Replace console.log with logger

* Add logging around `buildRecordUpdatePayload`

* Add details if field isn't being caught

* Revert change

* Type fixes

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-18 19:51:02 +00:00
Tushar Bhatt ff92979311 fix: spacing (#20172) 2025-03-18 17:37:24 +00:00
Anik Dhabal Babu f63edef48d fix: flaky e2e tests (#20168)
* fix: flaky e2e tests

* update

* fix
2025-03-18 22:30:00 +05:30
Rajiv Sahal 37e56ebbf9 dont auto accept user into an org when creating platform org (#20125) 2025-03-18 16:12:26 +00:00
07c38f1f33 test: Add tests for Booker component (#19051)
* feat: Don't allow unavailable slot's booking form to be visible - Send back to slots listing

* Improvements

* feat: Add env variables and strategies to prevent booking failures

- Added new environment variables for configuring slot reservation and availability checks
- Implemented strategies to handle concurrent bookings and slot availability
- Updated Booker component and related files to support new reservation and slot checking mechanisms
- Added README with detailed explanation of booking prevention strategies
- Configured dynamic intervals for slot and reservation queries

* docs: Update Booker README with detailed slot reservation and availability explanation

- Clarified slot reservation behavior when multiple users access the same booking page
- Added details about `getSchedule` refetching strategies and timing
- Explained how slot availability is detected and communicated to users

* Support for skip confirmation form flow

* feat: Enhance slot availability and reservation mechanism

- Refactored slot availability checking to support multiple slots
- Added tentative selected timeslots to improve booking UX
- Updated trpc handler to check availability for multiple slots
- Introduced new store methods for managing tentative slot selections
- Improved slot reservation and availability status tracking

* refactor: Improve slot availability quick check mechanism

- Renamed and restructured slot availability check parameters
- Extracted quick availability checks into a separate hook
- Updated type definitions for slot status and availability checks
- Simplified slot availability logic in Booker and related components
- Maintained cached state for quick availability checks

* Add tests booker

* Support minimu booking check too

* change data-testid

* Rneame

* Fix race condition with uid cookie not set

* fix tests

* Remove unsded variable

* fixup! Merge branch 'main' into dont-allow-booking-form-with-unavaialble-slot

* Fix the unavailability bug when only seconds differ

* Handle column view that doesnt have selected date and also add tests for isTimeSlotAavailable

* Fi ts and unit tests

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-03-18 13:02:20 +01:00
Gene Parmesan Thomas 192238f4bf fix: UID cookie isn't accessible/created when in embed as it is third party context. (#19859)
* [CAL-5240] Embeds: UID cookie isn't accessible/created when in embed as it is third party context.

* comments
2025-03-18 16:22:55 +05:30
Lauris SkraucisandMorgan 94a26031a8 feat: v2 api phone based organization team event types (#20134)
* refactor: input event types service remove unused code

* feat: toggle if email field is hidden

* chore: ensure event type has email or attendee phone field required

* chore: ensure non-org team cant have phone only event type

* chore: ensure users cant have phone only event type

* chore: ensure org team can have phone only event type

* chore: bump libraries

* docs

* refactor: confirm failing test response message

* fix: unit test

* chore: default organizer booking location docs

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-03-18 11:34:35 +02:00
Joe Au-Yeung e9f20b1a0b fix: Salesforce - fallback to creating lead (#20137)
* Fallback creating lead

* Add tests

* Type fix

* Type fixes

* Type fix

* Fix tests
2025-03-18 14:29:09 +05:30
Morgan 7cb9e2b99b chore: create org membership should be an upsert apiv2 (#20129) 2025-03-17 21:07:32 +00:00
Morgan dd45d8cdbc fix: don't remind platform users in reminder cron (#20056) 2025-03-17 23:00:25 +02:00
Hariom Balhara 3a181f1dac fix: Salesforce Stricter Website querying to prevent wrong account match during contact owner check (#20133)
* Make website check stricter in contact ownership flow as well

* Add test
2025-03-17 17:34:36 -03:00
Morgan c3abfa51ba chore: stringify body for request interceptor/middleware (#20131)
* chore: stringify body for request interceptor/middleware

* rename jsonBody to jsonBodyString
2025-03-17 19:45:37 +00:00
Alex van Andel 6498af167a refactor: Delete dead code that is not safe to use (#20141) 2025-03-17 17:23:39 +00:00
Keith WilliamsandhbjORbj 30dce7bacd perf: Move apps procedures off of loggedInViewer (#20112)
* perf: Slim down loggedInViewer tRPC router

* Fixed trpc client calls for new routes

* Moved bookingUnconfirmedCount

* Moved getUserTopBanners to me router

* Moved shouldVerifyEmail to me router

* Moved i18n from public to its own router

* fix ssrInit

* fix ssrInit usage

* fix type check

* better naming

* fix

* fix

* Fix types

* Removed used of importHandler

* perf: Move apps procedures off of loggedInViewer

* Moved 2 more apps-related procedures

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-17 17:18:19 +00:00
Keith Williams 3be9e1bd38 perf: Remove slots router from publicViewer (#20128) 2025-03-17 09:28:40 -07:00
Lauris Skraucis db52f44293 fix: atoms making requests to customers' domains (#20084)
* refactor: centralize useSchedule hook with isInit

* refactor: centralize useSchedules hook with isInit

* refactor: useConnectedCalendars enabled if isInit

* chore: some type fixes

* chore: type fix

* fix: import paths

* driveby: oauth clients docs
2025-03-17 10:43:39 +02:00
Keith WilliamsandhbjORbj 20d6eb5200 perf: Slim down loggedInViewer tRPC router (#20111)
* perf: Slim down loggedInViewer tRPC router

* Fixed trpc client calls for new routes

* Moved bookingUnconfirmedCount

* Moved getUserTopBanners to me router

* Moved shouldVerifyEmail to me router

* Moved i18n from public to its own router

* fix ssrInit

* fix ssrInit usage

* fix type check

* better naming

* fix

* fix

* Fix types

* Removed used of importHandler

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-16 18:22:48 -07:00
Keith Williams 424d94f16d feat: Use Watchlist on sending email verifications (#20114) 2025-03-16 14:20:20 +00:00
GitHub Actions 73e7d6f986 chore: release v5.1.0
Post release / release (push) Canceled after 0s
2025-03-15 11:11:37 +00:00
Alex van Andel 145001d26e chore: Bump typescript version in @calcom/ui (#19766)
* chore: Bump typescript version in @calcom/ui

* Reduce required types from @calcom/types to just used ones
2025-03-15 06:43:05 -03:00
5d2d7ce979 fix: German translation for seats_availability text (#20086)
* Fix German translation for seats_availability text

* enhancements

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Tushar <tusharbhatt0135@gmail.com>
2025-03-15 08:12:04 +00:00
GitHub Actions 4dc21ab2b0 chore: release v5.0.20
Post release / release (push) Canceled after 0s
2025-03-14 19:07:01 +00:00
Anik Dhabal Babu df6ee73e05 fix: schedule.get unneccsary called from avaiablity view (#20094) 2025-03-14 18:31:35 +00:00
Keith Williams 587af33179 perf: Use built types for appRouter (#20083)
* perf: Use built types for appRouter

* Got it working just for server

* Added dependency to turbo.json
2025-03-14 10:42:45 -07:00
Morgan 3b381b037c fix: incorrect managed user username format on update (#20090) 2025-03-14 15:36:24 +00:00
0968fc56eb feat: add utm tracking on booking (#19965)
* add tracking to booking + pass to handleNewBooking

* save tracking to booking

* return undefined if no utm params

* add test for handleNewBooking

* fix type error in test

* refactor code

* code clean up

* code clean up

* add missing import

* fix type import

* add utm params to insights (#20063)

* add utm params to insights

* remove duplicates

* remove duplicate columns

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2025-03-14 08:21:55 -07:00
Lauris Skraucis 13044cb5b0 feat: v2 bookings createdAt filter (#20054)
* feat: add before and after updated filter to core handler

* feat: afterCreatedAt and beforeCreatedAt v2 bookings filters

* wip

* finish

* chore: bump libraries

* fix: tests

* refactor: implement feedback
2025-03-14 15:49:57 +02:00
Morgan 580b03f2b9 chore: handle conferencing callback missing state error (#20081) 2025-03-14 15:49:04 +02:00
amrit 543d4bb6b3 fix: invitati instead of inviati (#20085) 2025-03-14 17:57:19 +05:30
Sarvesh Bajaj 21d87d5a80 fix: No type errors in build (#20075) 2025-03-14 07:39:10 +00:00
Lauris Skraucis 194719e7ba docs: v2 set cal-api-version header automatically (#20042) 2025-03-14 07:37:14 +00:00
Benny JooandKeith Williams dc666fc770 perf: fix paths in tailwind preset + ignore node modules in Platform tailwind config (#20067)
* fix paths in packages/config/tailwind preset

* ignore node_modules in platform tailwind preset

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-14 04:18:16 -03:00
Morgan 50a76e6b27 fix: delegation credential not found (#20055) 2025-03-14 06:13:41 +00:00
Parteek malikandamrit be4d3170db fix: synced no-show state after mutation (#20015)
Co-authored-by: amrit <iamamrit27@gmail.com>
2025-03-14 05:49:59 +00:00
Anik Dhabal Babu 7b97002230 fix: flaky test in ooo e2e (#20073)
* fix: flaky test in ooo e2e

* tweak
2025-03-14 08:58:13 +05:30
Benny Joo 6319d63e1f fix: client-export warning for enterprise page (#20077) 2025-03-13 21:48:40 +00:00
Calcom BotandLingo.dev 2e18478b5c feat: update translations via @lingodotdev (#20074)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-13 20:08:24 +00:00
565 changed files with 4783 additions and 1379 deletions
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+6
View File
@@ -0,0 +1,6 @@
undecided:
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.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({
@@ -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,
@@ -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);
@@ -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 {
@@ -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
@@ -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);
});
@@ -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);
}
@@ -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(
@@ -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,
@@ -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,
@@ -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) {
@@ -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 },
@@ -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 });
@@ -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()}`,
@@ -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({
+164 -72
View File
@@ -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;
@@ -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 -1
View File
@@ -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";
+1 -1
View File
@@ -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";
+11 -2
View File
@@ -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) {
+1 -1
View File
@@ -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,
+3 -3
View File
@@ -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();
},
});
+13 -36
View File
@@ -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 -1
View File
@@ -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;
},
+5 -8
View File
@@ -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);
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.0.19",
"version": "5.1.2",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+4
View File
@@ -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");
+4
View File
@@ -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");
+2
View File
@@ -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();
+2
View File
@@ -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();
+6
View File
@@ -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"]');
+2
View File
@@ -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!);
+8
View File
@@ -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);
}
+9 -6
View File
@@ -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.",
+30 -1
View File
@@ -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