Compare commits

...
88 Commits
Author SHA1 Message Date
Omar López 8685862617 v3.8.5
release.yml / release (push) Canceled after 0s
2024-02-29 10:26:06 -07:00
zomars 7422488b5d Update yarn.lock 2024-02-28 15:51:40 -07:00
zomars c3e1481d6a Update yarn.lock 2024-02-28 15:41:35 -07:00
Rushikesh a1d1883b34 Large padding on small screens for embedded booking widgets. (#13855) 2024-02-28 14:17:06 +00:00
Crowdin Bot f2ae94a190 New Crowdin translations by Github Action 2024-02-28 14:12:14 +00:00
Alex van Andel b82a597645 fix: invalid type check occupied seats (#13830)
* fix: Type check failure in occupiedSeats

* fix: Invalid handling of occupied seats hidden by earlier error
2024-02-28 11:07:34 -03:00
sean-brydonandUdit Takkar 5ba0ee8e85 fix: impersonation detecting orgs correctly. (#13889)
* feat:fix correctly detect impersonation organization

* chore: remove logs

* Update packages/features/ee/impersonation/lib/ImpersonationProvider.ts

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-28 12:16:46 +00:00
PritamandUdit Takkar 1463f97f14 fix: Currency Not Reflecting Correctly #13845 (#13847)
* fix currency problem

* fix type errors

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-28 17:14:26 +05:30
Nikhil Anand 34fea35e83 fix: text overflowing in username change popup #13846 (#13911) 2024-02-28 15:06:41 +05:30
Alex van AndelandUdit Takkar 2d75048037 fix: Test flakiness due to unfilled name/email (#13900)
* fix: Test flakiness due to unfilled name/email

* Better toast-success checks

* Revert "test: Create E2E tests for bookings with custom/required Phone + other questions (#11502)"

This reverts commit 614741d207.

* Revert "test: Create E2E tests for bookings with custom/required Multiple Emails + other questions  (teste2e-multipleEmailQuestion) (#11565)"

This reverts commit d1d50b0d91.

* Revert "test: Create E2E tests for bookings with custom/required Long Text + other questions (teste2e-longTextQuestion) (#11559)"

This reverts commit 96810b5ba1.

* Revert "test: Check the webhook event tab and your funtionalities (#12433)"

This reverts commit 9db9204088.

* Re-instate regularBookings

* Fixtures used by other tests

* Removed dead code from regularBookings.ts

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-28 06:07:32 +00:00
Carina WollendorferandCarinaWolli b549735334 santize right before sending mail (#13876)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-02-27 17:57:26 -03:00
Anwar SadathandCarinaWolli 86c379817e fix: Fix calendar event included in the workflow, doesn't show the meeting link (#13872)
* Fix calendar event included in the workflow, doesn't show the meeting link

* Fix type error

* fix public video call url

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-02-27 15:50:40 +00:00
Udit Takkar 5d1976295c fix: hover state (#13862)
release.yml / release (push) Canceled after 0s
2024-02-27 14:32:59 +00:00
Morgan e9b1fa1f50 v3.8.4 2024-02-27 16:31:14 +02:00
Udit Takkar 80080dda7a fix: missing userPrimaryEmail in workflows (#13875)
* fix: missing userPrimaryEmail in workflows

* chore

* fix: type err
2024-02-27 09:17:34 -05:00
Udit Takkar 27c25e1f95 chore: use transaction (#13874)
* fix: use transaction

* chore
2024-02-27 10:17:46 -03:00
Shyam Raghuwanshi 858be0f5d8 fix ics-feedcalendar logo not showing (#13886) 2024-02-27 11:43:25 +00:00
Hariom Balhara 68d0090f50 Make sure that state doesnt reset (#13880) 2024-02-27 07:51:11 -03:00
Udit Takkar a43fa13b11 fix: infinite rendering in event setup tab (#13877) 2024-02-26 22:18:58 +00:00
Pritam 2bf6da30b3 fix periodDays dirtystate (#13851) 2024-02-26 19:17:53 +00:00
Alex van Andel d85e0b51e4 fix: Prevent duplicate bookings with idempotency key (#13865)
* fix: Prevent duplicate bookings with idempotency key

* Add idempotencyKey to builder.ts type fix

* Add tests for new idempotency key (booking create)

* Add endTime to idempotency key, it should be possible to book different durations

* Move reschedule before the create so the idempotency key is unset
2024-02-26 18:33:22 +00:00
Carina WollendorferandCarinaWolli 55a317d677 fix: video call url in event location for recurring events (#13842)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-02-26 10:44:07 -05:00
Crowdin Bot 5fff8fc58c New Crowdin translations by Github Action 2024-02-26 14:36:48 +00:00
Udit Takkar 0d68767d0a fix: seed script (#13868) 2024-02-26 14:31:25 +00:00
Shyam Raghuwanshi 2051e87436 fix: username overflow (#13832) 2024-02-23 23:01:43 +00:00
Udit Takkar 77a157a12b fix: show go back button after expiry (#13833) 2024-02-23 22:20:46 +00:00
Omar López c4f25f2c44 build: deploy script for gradual migration (#13834) 2024-02-23 12:38:13 -07:00
Peer Richelsen d967facb63 feat: demodesk app (#13733) 2024-02-23 10:14:37 -05:00
c6dad39fb0 test: Create E2E tests for Analytics Apps (teste2e-analyticsApps) (#13695)
* refactor

* refactor

* refactor

* refactor

* fix failing check

* Update apps.ts

* fix failing check

* fix failing check

* fix failing check

---------

Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
2024-02-22 21:54:49 -03:00
zomars eec7f4d7d4 Update yarn.lock 2024-02-22 15:00:52 -07:00
Udit Takkar 95d5824ba9 fix: og image (#13829) 2024-02-22 10:46:33 -07:00
Somay Chauhan 5f95a4d041 fix: New Team Members Join Org as OWNER/ADMIN (#13822) 2024-02-22 11:46:28 -05:00
Keith Williams 8e97444129 v3.8.3
release.yml / release (push) Canceled after 0s
2024-02-22 10:16:18 -03:00
Crowdin Bot 8d8264b5f5 New Crowdin translations by Github Action 2024-02-22 12:48:57 +00:00
2ad81221d0 chore: Added a period after undone (#13824)
* Added period after undone

* chore: undo yarn.lock

* Revert yarn.lock

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-22 09:44:43 -03:00
3d1ac4523f fix: Update credentialId when null for a given selectedCalendar (#13735)
* fix: Update credentialId when null for a given selectedCalendar

* Add returnTo return path

* chore

* chore: undo return

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-02-22 12:05:46 +00:00
Alex van Andel b38807cb0f fix: Type check failure in occupiedSeats (#13805) 2024-02-22 10:27:55 +00:00
Peer Richelsen 11fb387370 feat: let org admins change usernames (#13384) 2024-02-22 07:48:46 +00:00
Crowdin Bot 1546f9115d New Crowdin translations by Github Action 2024-02-22 07:42:13 +00:00
Peer RichelsenandUdit Takkar 40ecd5dd9d chore: removing wrong translations about {{consoleUrl}} (#13802)
* removing wrong translations

* fixed source string

* revert: eng translation

* chore: remove translation

* chore

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-02-22 07:38:47 +00:00
Omar López ac6ac14a23 fix: more og avatar fixes (#13817) 2024-02-22 07:25:31 +00:00
Bailey Pumfleet c6534160e4 chore: Shorten "aplicaciones instaladas" to "apps instaladas" (#13806) 2024-02-22 07:21:43 +00:00
Keith Williams aa2db562b8 fix: Bookings API (#13815) 2024-02-21 16:05:53 -07:00
Carina WollendorferandCarinaWolli b7ed314be2 fix: log more info on sendEmail error (#13813)
* add from and subject to error message

* remove comment

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-02-21 20:59:45 +00:00
Udit TakkarandCarinaWolli d102268b33 fix: sdk action events (#13755)
* fix: sdk action events

* fix: type error

* fix: type err

* fix: reschedule event

* chore: remove timezone

* fix: feedback

* chore: use bookingInfo

* fix type error

* fix type errror

* add cancellation reason

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-02-21 14:25:24 -05:00
Somay ChauhanandUdit Takkar f4397d6593 fix: error on signup page while inviting a member to an org (#13807)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-21 18:49:51 +00:00
Keith Williams 192603a1fa fix: No available users found near midnight (#13801)
* fix: No available users found near midnight

* Update date-ranges.ts
2024-02-21 10:58:38 -07:00
Keith Williams a3d969680f chore: Including original start and end dates from request in log (#13800) 2024-02-21 13:22:42 -03:00
Crowdin Bot adc3b3aec6 New Crowdin translations by Github Action 2024-02-21 15:27:50 +00:00
sean-brydonandUdit Takkar 92286e2453 feat: signup redirect query param after onboarding (#13761)
* feat:redirect - if signup url has url to goto

* fix: dont throw if url is invalid - just dont set

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-21 20:53:09 +05:30
51b2a15cb0 feat: Settings for dashboard theme that is separate from system (#13779)
* feat(db): add new col: user appTheme

* feat(event-types page): apply appTheme color

* add appTheme for trpc

* feat: mutate main page theme

* feat: add english translation for app theme update section

* modify keys in common (en)

* add woring

* Revert yarn.lock to its state before unintended changes

* feat(i18n): add back the i18n string (for dark/light mode switching)

* refactor: refactor ThemeLabel

* fix: add new user field "appTheme" to test files

* chore: modify TODO comment

* chore: update common.json (en) for "theme"

since we had appTheme now, I think it's better to update the wording here to avoid confusion

* fix: update button's data-testid to fix e2e test error

* chore: remove comment

* fix: fix brand-color-not-apply bug

* solve type error

* fix: fix type error

* fix: use correct storageKey for booker / dashboard

* fix: tidy up

* fix: skeleton

---------

Co-authored-by: swh00tw <a6140000@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-02-21 13:02:40 +00:00
Morgan fa9a099dd6 fix: prefillFormParams guest type (#13798) 2024-02-21 12:36:48 +00:00
ErikandOmar López 83ceb46cca chore: Refactor feature flags (#13734)
Co-authored-by: Omar López <zomars@me.com>
2024-02-20 21:49:48 -03:00
Keith Williams 75d5576e17 v3.8.2
release.yml / release (push) Canceled after 0s
2024-02-20 21:41:13 -03:00
Keith Williams 0a6dde4fcc chore: Add extra logging for no available users error (#13791)
* chore: Add extra logging for no available users error

* Changed warn to info
2024-02-20 23:51:31 +00:00
Omar López 73b5839284 fix: og images avatars (#13790)
* fix og images avatars

* Update OgImages.tsx

* More fixes

* Update getPublicEvent.ts

* Update HeadSeo.tsx
2024-02-20 23:04:59 +00:00
Peer Richelsen 1a42b94b13 feat: added linears google calendar app (#13789) 2024-02-20 20:08:08 +00:00
Erik acb910f680 v3.8.1
release.yml / release (push) Canceled after 0s
2024-02-20 15:20:09 -03:00
Crowdin Bot 38e857f9a9 New Crowdin translations by Github Action 2024-02-20 15:30:02 +00:00
Crowdin Bot b8149dee36 New Crowdin translations by Github Action 2024-02-20 15:25:22 +00:00
Crowdin Bot f583ef40f2 New Crowdin translations by Github Action 2024-02-20 15:19:54 +00:00
Keith WilliamsandOmar López 71574488c5 fix: No available users found (#13738)
* fix: No available users found

* Update getUserAvailability.ts

* Update getUserAvailability.ts

* Update handleNewBooking.ts

* Reusing the parsed dates throughout the full stack

* Fixed typo

* Added unit tests for stringToDayjs

* Fixing types

* Reverted the getSchedule utc change since that endpoint takes in UTC formatted dates but with a given timezone

* chore: Add test replicating 'no available users' error

* Test is passing

* Name clarifications

* Test clean up

* Update packages/features/bookings/lib/handleNewBooking.ts

Co-authored-by: Omar López <zomars@me.com>

* Update handleNewBooking.ts

* Fixed issue where date overrides ending at midnight don't allow bookings

* Removed comment

---------

Co-authored-by: Omar López <zomars@me.com>
2024-02-20 12:15:05 -03:00
Omar López 008e0e43a2 chore: Drop deprecated password column (#13698) 2024-02-20 14:52:51 +00:00
Crowdin Bot da22eda662 New Crowdin translations by Github Action 2024-02-20 10:27:49 +00:00
sean-brydonandUdit Takkar f94d696885 fix: text-overflow (#13775)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-20 10:23:49 +00:00
Crowdin Bot 9e84eb1b2c New Crowdin translations by Github Action 2024-02-19 21:01:08 +00:00
zomars 4c549811a1 chore: Add explainer to RawHtml 2024-02-19 13:57:01 -07:00
Hariom Balhara 05502f0cfb fix: Old username link on org user's profile. (#13753) 2024-02-19 19:09:11 +00:00
mokkin e436b8e06b fix: correct German translation (#13742) 2024-02-19 19:07:18 +00:00
Crowdin Bot 8146cf8c93 New Crowdin translations by Github Action 2024-02-19 16:55:08 +00:00
Henrik KleeandKeith Williams c05f595ef0 reimplement #12681 (#13639)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-19 13:51:09 -03:00
Crowdin Bot 0a795f5363 New Crowdin translations by Github Action 2024-02-16 17:53:19 +00:00
5b50a469b2 fix: double bookings on seated event (#13406)
* fix: double bookings on seated event

* test: correcting handleSeats.test.ts

fix: seat rescheduling

* Add test for booking w/o bookingUid in req

* Add back `bookingUid`

* add 'reqBookingUid' parameter

* Add `reqBookingUid`

* Fix

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2024-02-16 12:48:57 -05:00
Alex van Andel c08365f09c feat: Implement orgs/teams logoUrls (#13699)
* feat: Implement orgs/teams logoUrls

* Use getAvatarUrl in team/[slug]

* Extract uploadLogo to standalone file for reuse

* Allow removing the team/organisation logo

* fix: API build fail due to server-only, removed
2024-02-16 10:35:53 -03:00
Shyam Raghuwanshi 477e102388 feat: Split-large-code-file-functions (#13592)
* Split large code file functions

* move the files from root into lib
2024-02-16 10:32:00 -03:00
Udit Takkar e1b10b4ec5 fix: update email flaky test (#13727) 2024-02-16 10:29:08 -03:00
Hariom BalharaandUdit Takkar ff6db3c572 fix: team invite signup not working (#13717)
* Fix team invite signup not working

* Add unit test

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-16 12:50:07 +00:00
Crowdin Bot 4fa3fdef97 New Crowdin translations by Github Action 2024-02-16 12:35:41 +00:00
Hariom Balhara f29f6632e8 fix: Managed children event listing for organization members (#13722)
* Fetch event-types by userId for childEvents and filter out those events that dont belong to the user

* Add test
2024-02-16 09:32:05 -03:00
Crowdin Bot 36747988e6 New Crowdin translations by Github Action 2024-02-16 11:41:38 +00:00
Morgan 3dd312b88b chore: booker web wrapper (#13676)
* chore: booker web wrapper

* fixup! chore: booker web wrapper

* fixup! fixup! chore: booker web wrapper

* fixup! fixup! fixup! chore: booker web wrapper

* fixup! fixup! fixup! fixup! chore: booker web wrapper

* fix: CreateANewOrganizationForm verify dialog

* fix: EventBooker not rerendering when it should

* fix: init booker store properly

* fixup! fix: init booker store properly

* fix: useMemo EventBooker

* fix: remove unnecessary file
2024-02-16 11:38:12 +00:00
Crowdin Bot 3c9e9329b0 New Crowdin translations by Github Action 2024-02-16 10:58:45 +00:00
Udit Takkar 8617f43a8e chore: add toast and tooltip (#13724)
* chore: add toast and tooltop

* chore: update text
2024-02-16 10:54:31 +00:00
Crowdin Bot 5f0eb652b0 New Crowdin translations by Github Action 2024-02-16 08:35:42 +00:00
JackandUdit Takkar 26d7301885 fix: [CAL-3141] (+1) badge not aligned (#13718)
* Fixes [CAL-3141] (+1) badge not aligned

* fix: centering

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-02-16 08:32:46 +00:00
Crowdin Bot ffc4d22ff1 New Crowdin translations by Github Action 2024-02-15 22:47:32 +00:00
ee286a600f refactor: Only send dirty fields for event type update call (#13292)
* init

* typefix maybe

* fix type

* fix e2e

* Extract delete dialog to its own function to optimise rerender

* wip

* Delete isEqual

* adds eslint tsignore comment

* only update changed fields

* fix type err

* --WIP

* fix type err isLoading—>isPending

* isLoading—>isPending

* post conflict resoution fix

* shouldDirty minimumBookingNotice

* adds shouldDirty to setValues

* reset form to ensure updated default values

* shouldDirty changes and fixed deep check of dirty

* --WIP

* should dirty recurring

* shouldDirty team tab

* multiple-fixes

* adding console log to test something in e2e

* shouldDirty apps tab

* fixes managed event type children association

* fixed managed events update

* dirty managed fix

* fixes some onChange versus setValue and periodCountCalendarDays

* Seeing different behaviour explicitly unwrapping formState

* only mutate if at least 1 field dirty

* fix type error

* bunch of fixes

* --WIP

* update only dirtyFields for Managed Events existing children

* Fixes update old managed event type tests --WIP

* Fix unit tests

A bunch of unit test fixes which started failing
due to update fields only requiring actual updated
fields. some other small fixes also included

* type fix

* remove unnecessary comment

* Adds dirtyField to instant event

* fix: add shouldDirty

* fixes availability and team tabs, and disable save button unless dirty

* fix for hosts and bookerLayout

* add shouldDirty when resetting hosts after changing schedulingType

* add missing shouldDirty to useEventTypeDestinationCalendarEmail

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-02-15 22:44:20 +00:00
Udit Takkar 72c493ee21 chore: add instruction for #12821 (#13713)
release.yml / release (push) Canceled after 0s
2024-02-16 01:22:12 +05:30
216 changed files with 5298 additions and 6088 deletions
+4
View File
@@ -74,6 +74,7 @@ export const schemaUserBaseBodyParams = User.pick({
timeZone: true,
weekStart: true,
theme: true,
appTheme: true,
defaultScheduleId: true,
locale: true,
hideBranding: true,
@@ -100,6 +101,7 @@ const schemaUserEditParams = z.object({
hideBranding: z.boolean().optional(),
timeZone: timeZone.optional(),
theme: z.nativeEnum(theme).optional().nullable(),
appTheme: z.nativeEnum(theme).optional().nullable(),
timeFormat: z.nativeEnum(timeFormat).optional(),
defaultScheduleId: z
.number()
@@ -122,6 +124,7 @@ const schemaUserCreateParams = z.object({
hideBranding: z.boolean().optional(),
timeZone: timeZone.optional(),
theme: z.nativeEnum(theme).optional().nullable(),
appTheme: z.nativeEnum(theme).optional().nullable(),
timeFormat: z.nativeEnum(timeFormat).optional(),
defaultScheduleId: z
.number()
@@ -159,6 +162,7 @@ export const schemaUserReadPublic = User.pick({
weekStart: true,
endTime: true,
bufferTime: true,
appTheme: true,
theme: true,
defaultScheduleId: true,
locale: true,
+1
View File
@@ -24,6 +24,7 @@
"@types/node": "16.9.1",
"@types/react": "18.0.26",
"@types/react-dom": "^18.0.9",
"@types/swagger-ui-react": "^4.18.3",
"typescript": "^4.9.4"
}
}
+2 -1
View File
@@ -1,9 +1,10 @@
import "highlight.js/styles/default.css";
import { type AppProps } from "next/app";
import "swagger-ui-react/swagger-ui.css";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
+1 -4
View File
@@ -1,9 +1,8 @@
import dynamic from "next/dynamic";
import type { SwaggerUI } from "swagger-ui-react";
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
const SwaggerUIDynamic: SwaggerUI & { url: string } = dynamic(() => import("swagger-ui-react"), {
const SwaggerUIDynamic = dynamic(() => import("swagger-ui-react"), {
ssr: false,
});
@@ -17,8 +16,6 @@ export default function APIDocs() {
requestSnippets={requestSnippets}
plugins={[SnippedGenerator]}
tryItOutEnabled={true}
syntaxHighlight={true}
enableCORS={false} // Doesn't seem to work either
docExpansion="list"
filter={true}
/>
+20 -1
View File
@@ -1,6 +1,7 @@
import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RecurringEvent } from "@calcom/types/Calendar";
@@ -23,13 +24,22 @@ type Props = {
theme: string | null;
allRemainingBookings: boolean;
seatReferenceUid?: string;
bookingCancelledEventProps: {
booking: unknown;
organizer: {
name: string;
email: string;
timeZone?: string;
};
eventType: unknown;
};
};
export default function CancelBooking(props: Props) {
const [cancellationReason, setCancellationReason] = useState<string>("");
const { t } = useLocale();
const router = useRouter();
const { booking, allRemainingBookings, seatReferenceUid } = props;
const { booking, allRemainingBookings, seatReferenceUid, bookingCancelledEventProps } = props;
const [loading, setLoading] = useState(false);
const telemetry = useTelemetry();
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
@@ -97,8 +107,17 @@ export default function CancelBooking(props: Props) {
method: "POST",
});
const bookingWithCancellationReason = {
...(bookingCancelledEventProps.booking as object),
cancellationReason,
} as unknown;
if (res.status >= 200 && res.status < 300) {
// tested by apps/web/playwright/booking-pages.e2e.ts
sdkActionManager?.fire("bookingCancelled", {
...bookingCancelledEventProps,
booking: bookingWithCancellationReason,
});
router.refresh();
} else {
setLoading(false);
@@ -1,3 +1,4 @@
import { InfoIcon } from "lucide-react";
import dynamic from "next/dynamic";
import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
@@ -59,7 +60,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const selectedThemeIsDark =
user?.theme === "dark" ||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
formMethods.getValues().bookingFields.forEach(({ name }) => {
bookingFields[name] = `${name} input`;
});
@@ -101,7 +101,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
};
}
return field;
})
}),
{ shouldDirty: true }
);
};
@@ -179,11 +180,20 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
{displayDestinationCalendarSelector && (
<div className="w-full">
<Switch
label={t("display_add_to_calendar_organizer")}
tooltip={t("reconnect_calendar_to_use")}
label={
<>
{t("display_add_to_calendar_organizer")}
<InfoIcon className="text-default hover:text-attention hover:bg-attention ms-1 inline h-4 w-4 rounded-md" />
</>
}
checked={useEventTypeDestinationCalendarEmail}
onCheckedChange={(val) => {
setUseEventTypeDestinationCalendarEmail(val);
formMethods.setValue("useEventTypeDestinationCalendarEmail", val);
formMethods.setValue("useEventTypeDestinationCalendarEmail", val, { shouldDirty: true });
if (val) {
showToast(t("reconnect_calendar_to_use"), "warning");
}
}}
/>
</div>
@@ -319,7 +329,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
description={t("private_link_description", { appName: APP_NAME })}
checked={hashedLinkVisible}
onCheckedChange={(e) => {
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
formMethods.setValue("hashedLink", e ? hashedUrl : undefined, { shouldDirty: true });
setHashedLinkVisible(e);
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
@@ -383,10 +393,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
// Enabling seats will disable guests and requiring confirmation until fully supported
if (e) {
toggleGuests(false);
formMethods.setValue("requiresConfirmation", false);
formMethods.setValue("requiresConfirmation", false, { shouldDirty: true });
setRequiresConfirmation(false);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2);
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2, {
shouldDirty: true,
});
} else {
formMethods.setValue("seatsPerTimeSlot", null);
toggleGuests(true);
@@ -506,7 +518,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
{showEventNameTip && (
<CustomEventTypeModal
close={closeEventNameTip}
setValue={(val: string) => formMethods.setValue("eventName", val)}
setValue={(val: string) => formMethods.setValue("eventName", val, { shouldDirty: true })}
defaultValue={formMethods.getValues("eventName")}
placeHolder={eventNamePlaceholder}
event={eventNameObject}
@@ -31,10 +31,14 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
const allAppsData = formMethods.watch("metadata")?.apps || {};
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
formMethods.setValue("metadata", {
...formMethods.getValues("metadata"),
apps: _allAppsData,
});
formMethods.setValue(
"metadata",
{
...formMethods.getValues("metadata"),
apps: _allAppsData,
},
{ shouldDirty: true }
);
};
const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => {
@@ -230,7 +230,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
);
setValue("availability", value);
setValue("availability", value, { shouldDirty: true });
},
[data]
);
@@ -238,7 +238,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
useEffect(() => {
if (!availabilityValue?.value) return;
setValue("schedule", availabilityValue.value);
setValue("schedule", availabilityValue.value, { shouldDirty: true });
}, [availabilityValue, setValue]);
return (
@@ -260,7 +260,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
isSearchable={false}
onChange={(selected) => {
field.onChange(selected?.value || null);
if (selected?.value) setValue("availability", selected);
if (selected?.value) setValue("availability", selected, { shouldDirty: true });
}}
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
value={availabilityValue}
@@ -298,7 +298,7 @@ const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSe
onCheckedChange={(checked) => {
onChange(!checked);
if (!checked) {
setValue("schedule", null);
setValue("schedule", null, { shouldDirty: true });
}
}}
title={t("choose_common_schedule_team_event")}
@@ -61,7 +61,8 @@ const MinimumBookingNoticeInput = React.forwardRef<
minimumBookingNoticeDisplayValues.type,
"minutes",
minimumBookingNoticeDisplayValues.value
)
),
{ shouldDirty: true }
);
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
@@ -230,7 +231,9 @@ export const EventLimitsTab = () => {
<Select
isSearchable={false}
onChange={(val) => {
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null, {
shouldDirty: true,
});
}}
defaultValue={
slotIntervalOptions.find(
@@ -258,11 +261,15 @@ export const EventLimitsTab = () => {
checked={isChecked}
onCheckedChange={(active) => {
if (active) {
formMethods.setValue("bookingLimits", {
PER_DAY: 1,
});
formMethods.setValue(
"bookingLimits",
{
PER_DAY: 1,
},
{ shouldDirty: true }
);
} else {
formMethods.setValue("bookingLimits", {});
formMethods.setValue("bookingLimits", {}, { shouldDirty: true });
}
}}
switchContainerClassName={classNames(
@@ -279,7 +286,7 @@ export const EventLimitsTab = () => {
/>
<Controller
name="onlyShowFirstAvailableSlot"
render={({ field: { value } }) => {
render={({ field: { onChange, value } }) => {
const isChecked = value;
return (
<SettingsToggle
@@ -289,7 +296,7 @@ export const EventLimitsTab = () => {
description={t("limit_booking_only_first_slot_description")}
checked={isChecked}
onCheckedChange={(active) => {
formMethods.setValue("onlyShowFirstAvailableSlot", active ?? false);
onChange(active ?? false);
}}
switchContainerClassName={classNames(
"border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
@@ -301,7 +308,7 @@ export const EventLimitsTab = () => {
/>
<Controller
name="durationLimits"
render={({ field: { value } }) => {
render={({ field: { onChange, value } }) => {
const isChecked = Object.keys(value ?? {}).length > 0;
return (
<SettingsToggle
@@ -317,11 +324,11 @@ export const EventLimitsTab = () => {
checked={isChecked}
onCheckedChange={(active) => {
if (active) {
formMethods.setValue("durationLimits", {
onChange({
PER_DAY: 60,
});
} else {
formMethods.setValue("durationLimits", {});
onChange({});
}
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
@@ -338,7 +345,7 @@ export const EventLimitsTab = () => {
/>
<Controller
name="periodType"
render={({ field: { value } }) => {
render={({ field: { onChange, value } }) => {
const isChecked = value && value !== "UNLIMITED";
return (
@@ -353,11 +360,18 @@ export const EventLimitsTab = () => {
title={t("limit_future_bookings")}
description={t("limit_future_bookings_description")}
checked={isChecked}
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
onCheckedChange={(bool) => {
if (bool && !formMethods.getValues("periodDays")) {
formMethods.setValue("periodDays", 30, { shouldDirty: true });
}
return onChange(bool ? "ROLLING" : "UNLIMITED");
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<RadioGroup.Root
value={watchPeriodType}
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
onValueChange={(val) =>
formMethods.setValue("periodType", val as PeriodType, { shouldDirty: true })
}>
{PERIOD_TYPES.map((period) => {
if (period.type === "UNLIMITED") return null;
return (
@@ -388,17 +402,21 @@ export const EventLimitsTab = () => {
options={optionsPeriod}
isSearchable={false}
onChange={(opt) =>
formMethods.setValue("periodCountCalendarDays", opt?.value === 1 ? "1" : "0")
formMethods.setValue(
"periodCountCalendarDays",
opt?.value === 1 ? true : false,
{ shouldDirty: true }
)
}
name="periodCoundCalendarDays"
value={optionsPeriod.find((opt) => {
opt.value ===
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0);
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0);
})}
defaultValue={optionsPeriod.find(
(opt) =>
opt.value ===
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0)
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0)
)}
/>
</div>
@@ -407,12 +425,12 @@ export const EventLimitsTab = () => {
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
<Controller
name="periodDates"
render={() => (
render={({ field: { onChange } }) => (
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
endDate={formMethods.getValues("periodDates").endDate}
onDatesChange={({ startDate, endDate }) => {
formMethods.setValue("periodDates", {
onChange({
startDate,
endDate,
});
@@ -446,7 +464,7 @@ export const EventLimitsTab = () => {
onCheckedChange={(active) => {
setOffsetToggle(active);
if (!active) {
formMethods.setValue("offsetStart", 0);
formMethods.setValue("offsetStart", 0, { shouldDirty: true });
}
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
@@ -577,11 +595,16 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
);
if (!rest || !currentKeys.length) return;
//currentDurationLimits is always defined so can be casted
// @ts-expect-error FIXME Fix these typings
setValue(propertyName, {
...watchIntervalLimits,
[rest.value]: defaultLimit,
});
setValue(
propertyName,
// @ts-expect-error FIXME Fix these typings
{
...watchIntervalLimits,
[rest.value]: defaultLimit,
},
{ shouldDirty: true }
);
};
return (
@@ -611,7 +634,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
)}
onLimitChange={(intervalLimitKey, val) =>
// @ts-expect-error FIXME Fix these typings
setValue(`${propertyName}.${intervalLimitKey}`, val)
setValue(`${propertyName}.${intervalLimitKey}`, val, { shouldDirty: true })
}
onDelete={(intervalLimitKey) => {
const current = currentIntervalLimits;
+10 -10
View File
@@ -83,7 +83,7 @@ const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
return mounted ? (
<Editor
getText={() => md.render(formMethods.getValues("description") || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
setText={(value: string) => formMethods.setValue("description", turndown(value), { shouldDirty: true })}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
editable={isEditable}
@@ -318,7 +318,7 @@ export const EventSetupTab = (
/>
</div>
<ErrorMessage
errors={formMethods.formState.errors.locations?.[index]}
errors={formMethods.formState.errors?.locations?.[index]}
name={eventLocationType.defaultValueVariable}
className="text-error my-1 ml-6 text-sm"
as="div"
@@ -512,16 +512,16 @@ export const EventSetupTab = (
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
if (newOptions.length > 0) {
setDefaultDuration(newOptions[0]);
formMethods.setValue("length", newOptions[0].value);
formMethods.setValue("length", newOptions[0].value, { shouldDirty: true });
} else {
setDefaultDuration(null);
}
}
if (newOptions.length === 1 && defaultDuration === null) {
setDefaultDuration(newOptions[0]);
formMethods.setValue("length", newOptions[0].value);
formMethods.setValue("length", newOptions[0].value, { shouldDirty: true });
}
formMethods.setValue("metadata.multipleDuration", values);
formMethods.setValue("metadata.multipleDuration", values, { shouldDirty: true });
}}
/>
</div>
@@ -542,7 +542,7 @@ export const EventSetupTab = (
setDefaultDuration(
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
);
if (option) formMethods.setValue("length", option.value);
if (option) formMethods.setValue("length", option.value, { shouldDirty: true });
}}
/>
</div>
@@ -571,12 +571,12 @@ export const EventSetupTab = (
setMultipleDuration(undefined);
setSelectedMultipleDuration([]);
setDefaultDuration(null);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
formMethods.setValue("length", eventType.length, { shouldDirty: true });
} else {
setMultipleDuration([]);
formMethods.setValue("metadata.multipleDuration", []);
formMethods.setValue("length", 0);
formMethods.setValue("metadata.multipleDuration", [], { shouldDirty: true });
formMethods.setValue("length", 0, { shouldDirty: true });
}
}}
/>
+18 -13
View File
@@ -127,7 +127,7 @@ const AssignAllTeamMembers = ({
labelClassName="mt-0.5 font-normal"
checked={assignAllTeamMembers}
onCheckedChange={(active) => {
setValue("assignAllTeamMembers", active);
setValue("assignAllTeamMembers", active, { shouldDirty: true });
setAssignAllTeamMembers(active);
if (active) {
onActive();
@@ -250,7 +250,8 @@ const FixedHosts = ({
isFixed: true,
userId: parseInt(teamMember.value, 10),
priority: 2,
}))
})),
{ shouldDirty: true }
)
}
/>
@@ -269,7 +270,7 @@ const FixedHosts = ({
const rrHosts = getValues("hosts")
.filter((host) => !host.isFixed)
.sort((a, b) => (b.priority ?? 2) - (a.priority ?? 2));
setValue("hosts", rrHosts);
setValue("hosts", rrHosts, { shouldDirty: true });
}
setIsDisabled(checked);
}}
@@ -290,7 +291,8 @@ const FixedHosts = ({
isFixed: true,
userId: parseInt(teamMember.value, 10),
priority: 2,
}))
})),
{ shouldDirty: true }
)
}
/>
@@ -332,7 +334,7 @@ const AddMembersWithSwitch = ({
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
onActive={onActive}
onInactive={() => setValue("hosts", [])}
onInactive={() => setValue("hosts", [], { shouldDirty: true })}
/>
</div>
) : (
@@ -395,7 +397,8 @@ const RoundRobinHosts = ({
userId: parseInt(teamMember.value, 10),
priority: 2,
}))
.sort((a, b) => b.priority - a.priority)
.sort((a, b) => b.priority - a.priority),
{ shouldDirty: true }
)
}
/>
@@ -420,7 +423,7 @@ const ChildrenEventTypes = ({
<AssignAllTeamMembers
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
onActive={() => setValue("children", childrenEventTypeOptions)}
onActive={() => setValue("children", childrenEventTypeOptions, { shouldDirty: true })}
/>
{!assignAllTeamMembers ? (
<Controller<FormValues>
@@ -449,7 +452,7 @@ const Hosts = ({
const { t } = useLocale();
const {
control,
resetField,
setValue,
getValues,
formState: { submitCount },
} = useFormContext<FormValues>();
@@ -469,10 +472,12 @@ const Hosts = ({
initialValue.current = { hosts: getValues("hosts"), schedulingType, submitCount };
return;
}
resetField("hosts", {
defaultValue: initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
});
}, [schedulingType, resetField, getValues, submitCount]);
setValue(
"hosts",
initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
{ shouldDirty: true }
);
}, [schedulingType, setValue, getValues, submitCount]);
return (
<Controller<FormValues>
@@ -586,7 +591,7 @@ export const EventTeamTab = ({
className="w-full"
onChange={(val) => {
onChange(val?.value);
setValue("assignAllTeamMembers", false);
setValue("assignAllTeamMembers", false, { shouldDirty: true });
setAssignAllTeamMembers(false);
}}
/>
@@ -14,6 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { SchedulingType } from "@calcom/prisma/enums";
import { trpc, TRPCClientError } from "@calcom/trpc/react";
import type { DialogProps } from "@calcom/ui";
import {
Button,
ButtonGroup,
@@ -133,6 +134,60 @@ function getNavigation({
];
}
function DeleteDialog({
isManagedEvent,
eventTypeId,
open,
onOpenChange,
}: { isManagedEvent: string; eventTypeId: number } & Pick<DialogProps, "open" | "onOpenChange">) {
const utils = trpc.useContext();
const { t } = useLocale();
const router = useRouter();
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => {
await utils.viewer.eventTypes.invalidate();
showToast(t("event_type_deleted_successfully"), "success");
router.push("/event-types");
onOpenChange?.(false);
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
onOpenChange?.(false);
} else if (err instanceof TRPCClientError) {
showToast(err.message, "error");
}
},
});
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<ConfirmationDialogContent
isPending={deleteMutation.isPending}
variety="danger"
title={t(`delete${isManagedEvent}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
onConfirm={(e) => {
e.preventDefault();
deleteMutation.mutate({ id: eventTypeId });
}}>
<p className="mt-5">
<Trans
i18nKey={`delete${isManagedEvent}_event_type_description`}
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
<ul>
<li>Members assigned to this event type will also have their event types deleted.</li>
<li>Anyone who they&apos;ve shared their link with will no longer be able to book using it.</li>
</ul>
</Trans>
</p>
</ConfirmationDialogContent>
</Dialog>
);
}
function EventTypeSingleLayout({
children,
eventType,
@@ -149,9 +204,7 @@ function EventTypeSingleLayout({
bookerUrl,
activeWebhooksNumber,
}: Props) {
const utils = trpc.useContext();
const { t } = useLocale();
const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const hasPermsToDelete =
@@ -160,24 +213,6 @@ function EventTypeSingleLayout({
formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
isUserOrganizationAdmin;
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => {
await utils.viewer.eventTypes.invalidate();
showToast(t("event_type_deleted_successfully"), "success");
router.push("/event-types");
setDeleteDialogOpen(false);
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
setDeleteDialogOpen(false);
} else if (err instanceof TRPCClientError) {
showToast(err.message, "error");
}
},
});
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
formMethods.getValues(),
t("locked_fields_admin_description"),
@@ -308,7 +343,7 @@ function EventTypeSingleLayout({
id="hiddenSwitch"
checked={!formMethods.watch("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", !e);
formMethods.setValue("hidden", !e, { shouldDirty: true });
}}
/>
</div>
@@ -424,7 +459,7 @@ function EventTypeSingleLayout({
id="hiddenSwitch"
checked={!formMethods.watch("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", !e);
formMethods.setValue("hidden", !e, { shouldDirty: true });
}}
/>
</div>
@@ -435,6 +470,7 @@ function EventTypeSingleLayout({
className="ml-4 lg:ml-0"
type="submit"
loading={isUpdateMutationLoading}
disabled={!formMethods.formState.isDirty}
data-testid="update-eventtype"
form="event-type-form">
{t("save")}
@@ -467,31 +503,13 @@ function EventTypeSingleLayout({
</div>
</div>
</Suspense>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<ConfirmationDialogContent
isPending={deleteMutation.isPending}
variety="danger"
title={t(`delete${isManagedEvent}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
onConfirm={(e) => {
e.preventDefault();
deleteMutation.mutate({ id: formMethods.getValues("id") });
}}>
<p className="mt-5">
<Trans
i18nKey={`delete${isManagedEvent}_event_type_description`}
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
<ul>
<li>Members assigned to this event type will also have their event types deleted.</li>
<li>
Anyone who they&apos;ve shared their link with will no longer be able to book using it.
</li>
</ul>
</Trans>
</p>
</ConfirmationDialogContent>
</Dialog>
<DeleteDialog
eventTypeId={eventType.id}
isManagedEvent={isManagedEvent}
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
/>
<EventTypeEmbedDialog />
</Shell>
);
@@ -81,10 +81,10 @@ export default function InstantEventController({
data-testid="instant-event-check"
onCheckedChange={(e) => {
if (!e) {
formMethods.setValue("isInstantEvent", false);
formMethods.setValue("isInstantEvent", false, { shouldDirty: true });
setInstantEventState(false);
} else {
formMethods.setValue("isInstantEvent", true);
formMethods.setValue("isInstantEvent", true, { shouldDirty: true });
setInstantEventState(true);
}
}}>
@@ -66,7 +66,7 @@ export default function RecurringEventController({
data-testid="recurring-event-check"
onCheckedChange={(e) => {
if (!e) {
formMethods.setValue("recurringEvent", null);
formMethods.setValue("recurringEvent", null, { shouldDirty: true });
setRecurringEventState(null);
} else {
const newVal = eventType.recurringEvent || {
@@ -74,7 +74,7 @@ export default function RecurringEventController({
count: 12,
freq: Frequency.WEEKLY,
};
formMethods.setValue("recurringEvent", newVal);
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
setRecurringEventState(newVal);
}
}}>
@@ -95,7 +95,7 @@ export default function RecurringEventController({
...recurringEventState,
interval: parseInt(event?.target.value),
};
formMethods.setValue("recurringEvent", newVal);
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
setRecurringEventState(newVal);
}}
/>
@@ -110,7 +110,7 @@ export default function RecurringEventController({
...recurringEventState,
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
};
formMethods.setValue("recurringEvent", newVal);
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
setRecurringEventState(newVal);
}}
/>
@@ -129,7 +129,7 @@ export default function RecurringEventController({
...recurringEventState,
count: parseInt(event?.target.value),
};
formMethods.setValue("recurringEvent", newVal);
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
setRecurringEventState(newVal);
}}
/>
@@ -37,7 +37,7 @@ export default function RequiresConfirmationController({
useEffect(() => {
if (!requiresConfirmation) {
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined, { shouldDirty: true });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [requiresConfirmation]);
@@ -82,7 +82,7 @@ export default function RequiresConfirmationController({
checked={requiresConfirmation}
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
onCheckedChange={(val) => {
formMethods.setValue("requiresConfirmation", val);
formMethods.setValue("requiresConfirmation", val, { shouldDirty: true });
onRequiresConfirmation(val);
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
@@ -96,16 +96,19 @@ export default function RequiresConfirmationController({
}
onValueChange={(val) => {
if (val === "always") {
formMethods.setValue("requiresConfirmation", true);
formMethods.setValue("requiresConfirmation", true, { shouldDirty: true });
onRequiresConfirmation(true);
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined, {
shouldDirty: true,
});
setRequiresConfirmationSetup(undefined);
} else if (val === "notice") {
formMethods.setValue("requiresConfirmation", true);
formMethods.setValue("requiresConfirmation", true, { shouldDirty: true });
onRequiresConfirmation(true);
formMethods.setValue(
"metadata.requiresConfirmationThreshold",
requiresConfirmationSetup || defaultRequiresConfirmationSetup
requiresConfirmationSetup || defaultRequiresConfirmationSetup,
{ shouldDirty: true }
);
}
}}>
@@ -146,7 +149,8 @@ export default function RequiresConfirmationController({
});
formMethods.setValue(
"metadata.requiresConfirmationThreshold.time",
val
val,
{ shouldDirty: true }
);
}}
className="border-default !m-0 block w-16 rounded-r-none border-r-0 text-sm [appearance:textfield] focus:z-10 focus:border-r"
@@ -171,7 +175,8 @@ export default function RequiresConfirmationController({
});
formMethods.setValue(
"metadata.requiresConfirmationThreshold.unit",
opt?.value as UnitTypeLongPlural
opt?.value as UnitTypeLongPlural,
{ shouldDirty: true }
);
}}
defaultValue={defaultValue}
@@ -37,7 +37,7 @@ const UserProfile = () => {
if (context.avatar) {
showToast(t("your_user_profile_updated_successfully"), "success");
await utils.viewer.me.refetch();
} else {
} else
try {
if (eventTypes?.length === 0) {
await Promise.all(
@@ -50,9 +50,11 @@ const UserProfile = () => {
console.error(error);
}
await utils.viewer.me.refetch();
router.push("/");
}
await utils.viewer.me.refetch();
const redirectUrl = localStorage.getItem("onBoardingRedirect");
localStorage.removeItem("onBoardingRedirect");
redirectUrl ? router.push(redirectUrl) : router.push("/");
},
onError: () => {
showToast(t("problem_saving_user_profile"), "error");
@@ -160,7 +160,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
<div className="bg-subtle flex w-full flex-wrap gap-6 rounded-sm px-2 py-3 text-sm">
<div>
<p className="text-subtle">{t("current_username")}</p>
<p className="text-emphasis mt-1" data-testid="current-username">
<p className="text-emphasis mt-1 break-all" data-testid="current-username">
{currentUsername}
</p>
</div>
@@ -168,7 +168,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
<p className="text-subtle" data-testid="new-username">
{t("new_username")}
</p>
<p className="text-emphasis mt-1">{inputUsernameValue}</p>
<p className="text-emphasis mt-1 break-all">{inputUsernameValue}</p>
</div>
</div>
</div>
+1 -1
View File
@@ -113,7 +113,7 @@ const enum ThemeSupport {
// e.g. Login Page
None = "none",
// Entire App except Booking Pages
App = "systemOnly",
App = "userConfigured",
// Booking Pages(including Routing Forms)
Booking = "userConfigured",
}
+5 -3
View File
@@ -127,9 +127,9 @@ const enum ThemeSupport {
// e.g. Login Page
None = "none",
// Entire App except Booking Pages
App = "systemOnly",
App = "appConfigured",
// Booking Pages(including Routing Forms)
Booking = "userConfigured",
Booking = "bookingConfigured",
}
type CalcomThemeProps = PropsWithChildren<
@@ -144,8 +144,10 @@ const CalcomThemeProvider = (props: CalcomThemeProps) => {
const embedNamespace = getEmbedNamespace(props.router.query);
const isEmbedMode = typeof embedNamespace === "string";
const themeProviderProps = getThemeProviderProps({ props, isEmbedMode, embedNamespace });
return (
<ThemeProvider {...getThemeProviderProps({ props, isEmbedMode, embedNamespace })}>
<ThemeProvider {...themeProviderProps}>
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
{typeof window !== "undefined" && !isEmbedMode && (
+3 -3
View File
@@ -1,11 +1,11 @@
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
// If feature flag is disabled, return not found on getServerSideProps
export const getServerSideProps = async () => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
const insightsEnabled = await getFeatureFlag(prisma, "insights");
if (flags.insights === false) {
if (!insightsEnabled) {
return {
notFound: true,
} as const;
@@ -1,12 +1,12 @@
import type { GetServerSidePropsContext } from "next";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
const organizations = await getFeatureFlag(prisma, "organizations");
// Check if organizations are enabled
if (flags["organizations"] !== true) {
if (!organizations) {
return {
notFound: true,
} as const;
+14 -3
View File
@@ -4,7 +4,7 @@ import { z } from "zod";
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername";
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import slugify from "@calcom/lib/slugify";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
@@ -24,11 +24,22 @@ const querySchema = z.object({
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
const signupDisabled = await getFeatureFlag(prisma, "disable-signup");
const ssr = await ssrInit(ctx);
const token = z.string().optional().parse(ctx.query.token);
const redirectUrlData = z
.string()
.refine((value) => value.startsWith(WEBAPP_URL), {
params: (value: string) => ({ value }),
message: "Redirect URL must start with 'cal.com'",
})
.optional()
.safeParse(ctx.query.redirect);
const redirectUrl = redirectUrlData.success && redirectUrlData.data ? redirectUrlData.data : null;
const props = {
redirectUrl,
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
isSAMLLoginEnabled,
trpcState: ssr.dehydrate(),
@@ -38,7 +49,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
// username + email prepopulated from query params
const { username: preFillusername, email: prefilEmail } = querySchema.parse(ctx.query);
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || flags["disable-signup"]) {
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || signupDisabled) {
return {
notFound: true,
} as const;
@@ -1,7 +1,7 @@
import type { GetServerSidePropsContext } from "next";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import logger from "@calcom/lib/logger";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
@@ -37,12 +37,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
// Provided by Rewrite from next.config.js
const isOrgProfile = context.query?.isOrgProfile === "1";
const flags = await getFeatureFlagMap(prisma);
const isOrganizationFeatureEnabled = flags["organizations"];
const organizationsEnabled = await getFeatureFlag(prisma, "organizations");
log.debug("getServerSideProps", {
isOrgProfile,
isOrganizationFeatureEnabled,
isOrganizationFeatureEnabled: organizationsEnabled,
isValidOrgDomain,
currentOrgDomain,
});
@@ -74,7 +73,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
if (
(!isValidOrgDomain && team?.parent) ||
(!isValidOrgDomain && !!metadata?.isOrganization) ||
!isOrganizationFeatureEnabled
!organizationsEnabled
) {
return { notFound: true } as const;
}
@@ -19,7 +19,6 @@ import { getEventName } from "@calcom/core/event";
import type { ConfigType } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import {
sdkActionManager,
useEmbedNonStylesConfig,
useIsBackgroundTransparent,
useIsEmbed,
@@ -193,22 +192,6 @@ export default function Success(props: PageProps) {
}, [telemetry]); */
useEffect(() => {
const users = eventType.users;
if (!sdkActionManager) return;
// TODO: We should probably make it consistent with Webhook payload. Some data is not available here, as and when requirement comes we can add
sdkActionManager.fire("bookingSuccessful", {
booking: bookingInfo,
eventType,
date: date.toString(),
duration: calculatedDuration,
organizer: {
name: users[0].name || "Nameless",
email: bookingInfo?.userPrimaryEmail || users[0].email || "Email-less",
timeZone: users[0].timeZone,
},
confirmed: !needsConfirmation,
// TODO: Add payment details
});
setDate(
date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess() || "Europe/London")
);
@@ -294,6 +277,16 @@ export default function Success(props: PageProps) {
const providerName = guessEventLocationType(location)?.label;
const rescheduleProviderName = guessEventLocationType(rescheduleLocation)?.label;
const bookingCancelledEventProps = {
booking: bookingInfo,
organizer: {
name: bookingInfo?.user?.name || "Nameless",
email: bookingInfo?.userPrimaryEmail || bookingInfo?.user?.email || "Email-less",
timeZone: bookingInfo?.user?.timeZone,
},
eventType,
};
return (
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
{!isEmbed && (
@@ -610,6 +603,7 @@ export default function Success(props: PageProps) {
theme={isSuccessBookingPage ? props.profile.theme : "light"}
allRemainingBookings={allRemainingBookings}
seatReferenceUid={seatReferenceUid}
bookingCancelledEventProps={bookingCancelledEventProps}
/>
</>
))}
@@ -425,7 +425,7 @@ export const EventTypeList = ({
<MemoizedItem type={type} group={group} readOnly={readOnly} />
<div className="mt-4 hidden sm:mt-0 sm:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
{type.team && !isManagedEventType && (
{!!type.teamId && !isManagedEventType && (
<UserAvatarGroup
className="relative right-3"
size="sm"
@@ -119,7 +119,7 @@ export type FormValues = {
schedule: number | null;
periodType: PeriodType;
periodDays: number;
periodCountCalendarDays: "1" | "0";
periodCountCalendarDays: boolean;
periodDates: { startDate: Date; endDate: Date };
seatsPerTimeSlot: number | null;
seatsShowAttendees: boolean | null;
@@ -192,14 +192,17 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
onSuccess: async () => {
formMethods.setValue(
"children",
formMethods.getValues().children.map((child) => ({
...child,
created: true,
}))
);
formMethods.setValue("assignAllTeamMembers", formMethods.getValues("assignAllTeamMembers") || false);
const currentValues = formMethods.getValues();
currentValues.children = currentValues.children.map((child) => ({
...child,
created: true,
}));
currentValues.assignAllTeamMembers = currentValues.assignAllTeamMembers || false;
// Reset the form with these values as new default values to ensure the correct comparison for dirtyFields eval
formMethods.reset(currentValues);
showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
},
async onSettled() {
@@ -253,7 +256,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
eventType.bookingFields.forEach(({ name }) => {
bookingFields[name] = name;
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const defaultValues: any = useMemo(() => {
return {
@@ -264,7 +266,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
beforeEventBuffer: eventType.beforeEventBuffer,
eventName: eventType.eventName || "",
scheduleName: eventType.scheduleName,
periodDays: eventType.periodDays || 30,
periodDays: eventType.periodDays,
requiresBookerEmailVerification: eventType.requiresBookerEmailVerification,
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
seatsShowAttendees: eventType.seatsShowAttendees,
@@ -290,7 +292,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
offsetStart: eventType.offsetStart,
bookingFields: eventType.bookingFields,
periodType: eventType.periodType,
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
periodCountCalendarDays: eventType.periodCountCalendarDays ? true : false,
schedulingType: eventType.schedulingType,
requiresConfirmation: eventType.requiresConfirmation,
slotInterval: eventType.slotInterval,
@@ -316,7 +318,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
assignAllTeamMembers: eventType.assignAllTeamMembers,
};
}, [eventType, periodDates, metadata]);
const formMethods = useForm<FormValues>({
defaultValues,
resolver: zodResolver(
@@ -405,14 +406,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
.passthrough()
),
});
useEffect(() => {
if (!formMethods.formState.isDirty) {
//TODO: What's the best way to sync the form with backend
formMethods.setValue("bookingFields", defaultValues.bookingFields);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultValues]);
const {
formState: { isDirty: isFormDirty, dirtyFields },
} = formMethods;
const appsMetadata = formMethods.getValues("metadata")?.apps;
const availability = formMethods.watch("availability");
@@ -453,8 +449,100 @@ const EventTypePage = (props: EventTypeSetupProps) => {
),
webhooks: <EventWebhooksTab eventType={eventType} />,
} as const;
const isObject = <T,>(value: T): boolean => {
return value !== null && typeof value === "object" && !Array.isArray(value);
};
const isArray = <T,>(value: T): boolean => {
return Array.isArray(value);
};
const isFieldDirty = (fieldName: keyof FormValues) => {
// If the field itself is directly marked as dirty
if (dirtyFields[fieldName] === true) {
return true;
}
// Check if the field is an object or an array
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fieldValue: any = getNestedField(dirtyFields, fieldName);
if (isObject(fieldValue)) {
for (const key in fieldValue) {
if (fieldValue[key] === true) {
return true;
}
if (isObject(fieldValue[key]) || isArray(fieldValue[key])) {
const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
// Recursive call for nested objects or arrays
if (isFieldDirty(nestedFieldName)) {
return true;
}
}
}
}
if (isArray(fieldValue)) {
for (const element of fieldValue) {
// If element is an object, check each property of the object
if (isObject(element)) {
for (const key in element) {
if (element[key] === true) {
return true;
}
if (isObject(element[key]) || isArray(element[key])) {
const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
// Recursive call for nested objects or arrays within each element
if (isFieldDirty(nestedFieldName)) {
return true;
}
}
}
} else if (element === true) {
return true;
}
}
}
return false;
};
const getNestedField = (obj: typeof dirtyFields, path: string) => {
const keys = path.split(".");
let current = obj;
for (let i = 0; i < keys.length; i++) {
// @ts-expect-error /—— currentKey could be any deeply nested fields thanks to recursion
const currentKey = current[keys[i]];
if (currentKey === undefined) return undefined;
current = currentKey;
}
return current;
};
const getDirtyFields = (values: FormValues): Partial<FormValues> => {
if (!isFormDirty) {
return {};
}
const updatedFields: Partial<FormValues> = {};
Object.keys(dirtyFields).forEach((key) => {
const typedKey = key as keyof typeof dirtyFields;
updatedFields[typedKey] = undefined;
const isDirty = isFieldDirty(typedKey);
if (isDirty) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
updatedFields[typedKey] = values[typedKey];
}
});
return updatedFields;
};
const handleSubmit = async (values: FormValues) => {
const { children } = values;
const dirtyValues = getDirtyFields(values);
const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
const {
periodDates,
periodCountCalendarDays,
@@ -470,7 +558,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
locations,
metadata,
customInputs,
children,
assignAllTeamMembers,
// We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -483,8 +570,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
multipleDurationEnabled,
length,
...input
} = values;
} = dirtyValues;
if (!Number(length)) throw new Error(t("event_setup_length_error"));
if (bookingLimits) {
@@ -504,8 +590,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!length && !metadata?.multipleDuration?.includes(length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
// if length is unchanged, we skip this check
if (length !== undefined) {
if (!length && !metadata?.multipleDuration?.includes(length)) {
//This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
}
}
@@ -521,14 +611,14 @@ const EventTypePage = (props: EventTypeSetupProps) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, users, scheduleName, ...rest } = input;
updateMutation.mutate({
const payload = {
...rest,
length,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
periodEndDate: periodDates.endDate,
periodCountCalendarDays: periodCountCalendarDays === "1",
periodStartDate: periodDates?.startDate,
periodEndDate: periodDates?.endDate,
periodCountCalendarDays,
id: eventType.id,
beforeEventBuffer,
afterEventBuffer,
@@ -542,7 +632,19 @@ const EventTypePage = (props: EventTypeSetupProps) => {
customInputs,
children,
assignAllTeamMembers,
});
};
// Filter out undefined values
const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
if (value !== undefined) {
// @ts-expect-error Element implicitly has any type
acc[key] = value;
}
return acc;
}, {});
if (dirtyFieldExists) {
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
}
};
const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState<ChildrenEventType[]>([]);
@@ -595,6 +697,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
form={formMethods}
id="event-type-form"
handleSubmit={async (values) => {
const { children } = values;
const dirtyValues = getDirtyFields(values);
const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
const {
periodDates,
periodCountCalendarDays,
@@ -617,9 +722,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
multipleDurationEnabled,
length,
...input
} = values;
} = dirtyValues;
if (!Number(length)) throw new Error(t("event_setup_length_error"));
if (length && !Number(length)) throw new Error(t("event_setup_length_error"));
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
@@ -638,8 +743,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!length && !metadata?.multipleDuration?.includes(length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
if (length !== undefined) {
if (!length && !metadata?.multipleDuration?.includes(length)) {
//This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
}
}
@@ -651,14 +759,15 @@ const EventTypePage = (props: EventTypeSetupProps) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, users, scheduleName, ...rest } = input;
updateMutation.mutate({
const payload = {
...rest,
children,
length,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
periodEndDate: periodDates.endDate,
periodCountCalendarDays: periodCountCalendarDays === "1",
periodStartDate: periodDates?.startDate,
periodEndDate: periodDates?.endDate,
periodCountCalendarDays,
id: eventType.id,
beforeEventBuffer,
afterEventBuffer,
@@ -670,7 +779,19 @@ const EventTypePage = (props: EventTypeSetupProps) => {
seatsShowAvailabilityCount,
metadata,
customInputs,
});
};
// Filter out undefined values
const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
if (value !== undefined) {
// @ts-expect-error Element implicitly has any type
acc[key] = value;
}
return acc;
}, {});
if (dirtyFieldExists) {
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
}
}}>
<div ref={animationParentRef}>{tabMap[tabName]}</div>
</Form>
@@ -7,6 +7,7 @@ import { handleUserRedirection } from "@calcom/features/booking-redirect/handle-
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import logger from "@calcom/lib/logger";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { safeStringify } from "@calcom/lib/safeStringify";
@@ -206,7 +207,10 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
const profile = {
name: user.name || user.username || "",
image: user.avatar,
image: getUserAvatarUrl({
...user,
profile: user.profile,
}),
theme: user.theme,
brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
avatarUrl: user.avatarUrl,
@@ -155,7 +155,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
<Link
prefetch={false}
href={{
pathname: `/${user.username}/${type.slug}`,
pathname: `/${user.profile.username}/${type.slug}`,
query,
}}
passHref
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.8.0",
"version": "3.8.5",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -95,6 +95,7 @@ const IntegrationsContainer = ({
className="mb-6"
actions={
<Button
data-testid="add-apps"
href={variant ? `/apps/categories/${variant}` : "/apps"}
color="secondary"
StartIcon={Plus}>
@@ -11,8 +11,10 @@ import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { APP_NAME } from "@calcom/lib/constants";
import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants";
import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
import type { userMetadata } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
@@ -29,6 +31,7 @@ import {
SkeletonText,
SettingsToggle,
UpgradeTeamsBadge,
useCalcomTheme,
} from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
@@ -41,10 +44,10 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
<SkeletonText className="h-8 w-1/3" />
</div>
<div className="border-subtle space-y-6 border-x px-4 py-6 sm:px-6">
<div className="flex items-center justify-center">
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
<div className="[&>*]:bg-emphasis flex w-full items-center justify-center gap-x-2 [&>*]:animate-pulse">
<div className="h-32 flex-1 rounded-md p-5" />
<div className="h-32 flex-1 rounded-md p-5" />
<div className="h-32 flex-1 rounded-md p-5" />
</div>
<div className="flex justify-between">
<SkeletonText className="h-8 w-1/3" />
@@ -62,6 +65,26 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
);
};
const useBrandColors = (
currentTheme: string | null,
{
brandColor,
darkBrandColor,
}: {
brandColor?: string | null;
darkBrandColor?: string | null;
}
): void => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
const selectedTheme = currentTheme ? brandTheme[currentTheme as "light" | "dark"] : {};
useCalcomTheme({
root: selectedTheme,
});
};
const AppearanceView = ({
user,
hasPaidPlan,
@@ -77,6 +100,22 @@ const AppearanceView = ({
user?.brandColor !== DEFAULT_LIGHT_BRAND_COLOR || user?.darkBrandColor !== DEFAULT_DARK_BRAND_COLOR
);
const [hideBrandingValue, setHideBrandingValue] = useState(user?.hideBranding ?? false);
useTheme(user?.appTheme);
useBrandColors(user?.appTheme ?? null, {
brandColor: user?.brandColor,
darkBrandColor: user?.darkBrandColor,
});
const userAppThemeFormMethods = useForm({
defaultValues: {
appTheme: user.appTheme,
},
});
const {
formState: { isSubmitting: isUserAppThemeSubmitting, isDirty: isUserAppThemeDirty },
reset: resetUserAppThemeReset,
} = userAppThemeFormMethods;
const userThemeFormMethods = useForm({
defaultValues: {
@@ -131,6 +170,7 @@ const AppearanceView = ({
resetBrandColorsThemeReset({ brandColor: data.brandColor, darkBrandColor: data.darkBrandColor });
resetBookerLayoutThemeReset({ metadata: data.metadata });
resetUserThemeReset({ theme: data.theme });
resetUserAppThemeReset({ appTheme: data.appTheme });
},
onError: (error) => {
if (error.message) {
@@ -147,6 +187,56 @@ const AppearanceView = ({
return (
<div>
<Meta title={t("appearance")} description={t("appearance_description")} borderInShellHeader={false} />
<div className="border-subtle mt-6 flex items-center rounded-t-lg border p-6 text-sm">
<div>
<p className="text-default text-base font-semibold">{t("app_theme")}</p>
<p className="text-default">{t("app_theme_applies_note")}</p>
</div>
</div>
<Form
form={userAppThemeFormMethods}
handleSubmit={(values) => {
mutation.mutate({
appTheme: values.appTheme ?? null,
});
}}>
<div className="border-subtle flex flex-col justify-between border-x px-6 py-8 sm:flex-row">
<ThemeLabel
variant="system"
value={undefined}
label={t("theme_system")}
defaultChecked={user.appTheme === null}
register={userAppThemeFormMethods.register}
fieldName="appTheme"
/>
<ThemeLabel
variant="light"
value="light"
label={t("light")}
defaultChecked={user.appTheme === "light"}
register={userAppThemeFormMethods.register}
fieldName="appTheme"
/>
<ThemeLabel
variant="dark"
value="dark"
label={t("dark")}
defaultChecked={user.appTheme === "dark"}
register={userAppThemeFormMethods.register}
fieldName="appTheme"
/>
</div>
<SectionBottomActions className="mb-6" align="end">
<Button
disabled={isUserAppThemeSubmitting || !isUserAppThemeDirty}
type="submit"
data-testid="update-app-theme-btn"
color="primary">
{t("update")}
</Button>
</SectionBottomActions>
</Form>
<div className="border-subtle mt-6 flex items-center rounded-t-lg border p-6 text-sm">
<div>
<p className="text-default text-base font-semibold">{t("theme")}</p>
@@ -374,7 +374,7 @@ const ProfileView = () => {
</div>
<DialogFooter showDivider>
<Button
data-testId="profile-update-email-submit-button"
data-testid="profile-update-email-submit-button"
color="primary"
loading={confirmPasswordMutation.isPending}
onClick={(e) => onConfirmPassword(e)}>
@@ -511,7 +511,7 @@ const ProfileForm = ({
<TextField
label={t("email")}
hint={t("change_email_hint")}
data-testId="profile-form-email"
data-testid="profile-form-email"
{...formMethods.register("email")}
/>
</div>
@@ -535,7 +535,7 @@ const ProfileForm = ({
disabled={isDisabled}
color="primary"
type="submit"
data-testId="profile-submit-button">
data-testid="profile-submit-button">
{t("update")}
</Button>
</SectionBottomActions>
+7
View File
@@ -159,6 +159,7 @@ export default function Signup({
isGoogleLoginEnabled,
isSAMLLoginEnabled,
orgAutoAcceptEmail,
redirectUrl,
}: SignupProps) {
const [premiumUsername, setPremiumUsername] = useState(false);
const [usernameTaken, setUsernameTaken] = useState(false);
@@ -180,6 +181,12 @@ export default function Signup({
formState: { isSubmitting, errors, isSubmitSuccessful },
} = formMethods;
useEffect(() => {
if (redirectUrl) {
localStorage.setItem("onBoardingRedirect", redirectUrl);
}
}, [redirectUrl]);
const loadingSubmitState = isSubmitSuccessful || isSubmitting;
const handleErrorsAndStripe = async (resp: Response) => {
+9 -12
View File
@@ -13,7 +13,7 @@ import { useEffect } from "react";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getOrgAvatarUrl, getTeamAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
@@ -123,7 +123,7 @@ function TeamPage({
(mem) => mem.subteams?.includes(ch.slug) && mem.accepted
).length;
return (
<li key={i} className="hover:bg-muted w-full">
<li key={i} className="hover:bg-muted w-full rounded-md">
<Link href={`/${ch.slug}`} className="flex items-center justify-between">
<div className="flex items-center px-5 py-5">
<div className="ms-3 inline-block truncate">
@@ -157,6 +157,11 @@ function TeamPage({
</div>
);
const profileImageSrc =
isValidOrgDomain || team.metadata?.isOrganization
? getOrgAvatarUrl({ slug: currentOrgDomain, logoUrl: team.logoUrl })
: getTeamAvatarUrl({ slug: team.slug, logoUrl: team.logoUrl, organizationId: team.parent?.id });
return (
<>
<HeadSeo
@@ -166,22 +171,14 @@ function TeamPage({
title: markdownStrippedBio,
profile: {
name: `${team.name}`,
image: `${WEBAPP_URL}/${team.metadata?.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`,
image: profileImageSrc,
},
}}
/>
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
<div className="mx-auto mb-8 max-w-3xl text-center">
<div className="relative">
<Avatar
alt={teamName}
imageSrc={
isValidOrgDomain
? `/org/${currentOrgDomain}/avatar.png`
: `${WEBAPP_URL}/${team.metadata?.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`
}
size="lg"
/>
<Avatar alt={teamName} imageSrc={profileImageSrc} size="lg" />
</div>
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
{team.parent && `${team.parent.name} `}
@@ -0,0 +1,35 @@
import { loginUser } from "../../fixtures/regularBookings";
import { test } from "../../lib/fixtures";
const ALL_APPS = ["fathom", "matomo", "plausible", "ga4", "gtm", "metapixel"];
test.describe("Check analytics Apps", () => {
test.beforeEach(async ({ page, users }) => {
await loginUser(users);
await page.goto("/apps/");
});
test("Check analytics Apps", async ({ appsPage, page }) => {
await appsPage.goToAppsCategory("analytics");
await appsPage.installApp("fathom");
await appsPage.goBackToAppsPage();
await appsPage.installApp("matomo");
await appsPage.goBackToAppsPage();
await appsPage.installApp("plausible");
await appsPage.goBackToAppsPage();
await appsPage.installApp("ga4");
await appsPage.goBackToAppsPage();
await appsPage.installApp("gtm");
await appsPage.goBackToAppsPage();
await appsPage.installApp("metapixel");
await appsPage.goBackToAppsPage();
await page.goto("/event-types");
await appsPage.goToEventType("30 min");
await appsPage.goToAppsTab();
await appsPage.verifyAppsInfo(0);
for (const app of ALL_APPS) {
await appsPage.activeApp(app);
}
await appsPage.verifyAppsInfo(6);
});
});
+29
View File
@@ -175,6 +175,35 @@ testBothFutureAndLegacyRoutes.describe("pro user", () => {
await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
});
test("can book an unconfirmed event multiple times", async ({ page, users }) => {
await page.locator('[data-testid="event-type-link"]:has-text("Opt in")').click();
await selectFirstAvailableTimeSlotNextMonth(page);
const pageUrl = page.url();
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
// go back to the booking page to re-book.
await page.goto(pageUrl);
await bookTimeSlot(page, { email: "test2@example.com" });
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
test("cannot book an unconfirmed event multiple times with the same email", async ({ page, users }) => {
await page.locator('[data-testid="event-type-link"]:has-text("Opt in")').click();
await selectFirstAvailableTimeSlotNextMonth(page);
const pageUrl = page.url();
await bookTimeSlot(page);
// go back to the booking page to re-book.
await page.goto(pageUrl);
await bookTimeSlot(page);
await expect(page.getByText("Could not book the meeting.")).toBeVisible();
});
test("can book with multiple guests", async ({ page, users }) => {
const additionalGuests = ["test@gmail.com", "test2@gmail.com"];
@@ -1,483 +0,0 @@
import { loginUser } from "../../fixtures/regularBookings";
import { test } from "../../lib/fixtures";
test.describe("Booking With Address Question and Each Other Question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test.beforeEach(async ({ page, users }) => {
await loginUser(users);
await page.goto("/event-types");
});
test.describe("Booking With Address Question and Checkbox Group Question", () => {
test("Address required and checkbox group required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Checkbox Group question (both required)",
secondQuestion: "checkbox",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Address and checkbox group not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Checkbox Group question (only address required)",
secondQuestion: "checkbox",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Address Question and Checkbox Question", () => {
test("Address required and checkbox required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Checkbox question (both required)",
secondQuestion: "boolean",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Addres and checkbox not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Checkbox question (only address required)",
secondQuestion: "boolean",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Address Question and Long text Question", () => {
test("Addres required and Long Text required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Long Text question (both required)",
secondQuestion: "textarea",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Address and Long Text not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Long Text question (only address required)",
secondQuestion: "textarea",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Address Question and Multi email Question", () => {
test("Address required and Multi email required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Multiemail question (both required)",
secondQuestion: "multiemail",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Address and Multi email not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
false,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Multiemail question (only address required)",
secondQuestion: "multiemail",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Address Question and multiselect Question", () => {
test("Address required and multiselect text required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Multi Select question (both required)",
secondQuestion: "multiselect",
options: { ...bookingOptions, isMultiSelect: true },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Address and multiselect text not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Multi Select question (only address required)",
secondQuestion: "multiselect",
options: { ...bookingOptions, isMultiSelect: true, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Address Question and Number Question", () => {
test("Address required and Number required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Number question (both required)",
secondQuestion: "number",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Address and Number not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Number question (only address required)",
secondQuestion: "number",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Address Question and Phone Question", () => {
test("Address required and Phone required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone-test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Multi Select question (both required)",
secondQuestion: "phone",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Address and Phone not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone-test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Multi Select question (only address required)",
secondQuestion: "phone",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Address Question and Radio group Question", () => {
test("Address required and Radio group required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Radio question (both required)",
secondQuestion: "radio",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Address and Radio group not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Radio question (only address required)",
secondQuestion: "radio",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Address Question and select Question", () => {
test("Address required and select required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Select question (both required)",
secondQuestion: "select",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Address and select not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("select", "select-test", "select test", false, "select test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Select question (both required)",
secondQuestion: "select",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Address Question and Short text question", () => {
test("Address required and Short text required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Multi Select question (both required)",
secondQuestion: "text",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Address and Short text not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "address",
fillText: "Test Address question and Multi Select question (only address required)",
secondQuestion: "text",
options: { ...bookingOptions, isRequired: true },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
});
@@ -1,110 +0,0 @@
/* eslint-disable playwright/no-conditional-in-test */
import { loginUser } from "../fixtures/regularBookings";
import { test } from "../lib/fixtures";
test.describe("Booking With All Questions", () => {
test.beforeEach(async ({ page, users, bookingPage }) => {
await loginUser(users);
await page.goto("/event-types");
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
});
const bookingOptions = { isAllRequired: true };
test("Selecting and filling all questions as required", async ({ bookingPage }) => {
const allQuestions = [
"phone",
"address",
"checkbox",
"boolean",
"textarea",
"multiemail",
"multiselect",
"number",
"radio",
"select",
"text",
];
for (const question of allQuestions) {
if (
question !== "number" &&
question !== "select" &&
question !== "checkbox" &&
question !== "boolean" &&
question !== "multiselect" &&
question !== "radio"
) {
await bookingPage.addQuestion(
question,
`${question}-test`,
`${question} test`,
true,
`${question} test`
);
} else {
await bookingPage.addQuestion(question, `${question}-test`, `${question} test`, true);
}
await bookingPage.checkField(question);
}
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAllQuestions(eventTypePage, allQuestions, bookingOptions);
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Selecting and filling all questions as optional", async ({ bookingPage }) => {
const allQuestions = [
"phone",
"address",
"checkbox",
"boolean",
"textarea",
"multiemail",
"multiselect",
"number",
"radio",
"select",
"text",
];
for (const question of allQuestions) {
if (
question !== "number" &&
question !== "select" &&
question !== "checkbox" &&
question !== "boolean" &&
question !== "multiselect" &&
question !== "radio"
) {
await bookingPage.addQuestion(
question,
`${question}-test`,
`${question} test`,
false,
`${question} test`
);
} else {
await bookingPage.addQuestion(question, `${question}-test`, `${question} test`, false);
}
await bookingPage.checkField(question, { isOptional: true });
}
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAllQuestions(eventTypePage, allQuestions, {
...bookingOptions,
isAllRequired: false,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
@@ -1,445 +0,0 @@
import { loginUser } from "../fixtures/regularBookings";
import { test } from "../lib/fixtures";
test.describe("Booking With Checkbox Group Question and Each Other Question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test.beforeEach(async ({ page, users, bookingPage }) => {
await loginUser(users);
await page.goto("/event-types");
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
});
test.describe("Booking With Checkbox Group Question and Address Question", () => {
test("Checkbox Group required and Address required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Address question (both required)",
secondQuestion: "address",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Checkbox Group and Address not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Address question (only checkbox required)",
secondQuestion: "address",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test.describe("Booking With Checkbox Group Question and Phone Question", () => {
test("Checkbox Group required and Phone required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and phone question (both required)",
secondQuestion: "phone",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Checkbox Group and Phone not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Phone question (only checkbox required)",
secondQuestion: "phone",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Checkbox Group Question and checkbox Question", () => {
test("Checkbox Group required and checkbox required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and checkbox question (both required)",
secondQuestion: "boolean",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Checkbox Group and checkbox not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and checkbox (only checkbox required)",
secondQuestion: "boolean",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Checkbox Group Question and Long text Question", () => {
test("Checkbox Group required and Long text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Long Text question (both required)",
secondQuestion: "textarea",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Checkbox Group and Long text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Long Text question (only checkbox required)",
secondQuestion: "textarea",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Checkbox Group Question and Multi email Question", () => {
test("Checkbox Group required and Multi email required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Multi Email question (both required)",
secondQuestion: "multiemail",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Checkbox Group and Multi email not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
false,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Multi Email question (only checkbox required)",
secondQuestion: "multiemail",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Checkbox Group Question and multiselect Question", () => {
test("Checkbox Group required and multiselect text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Multi Select question (both required)",
secondQuestion: "multiselect",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Checkbox Group and multiselect text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Multi Select question (only checkbox required)",
secondQuestion: "multiselect",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Checkbox Group Question and Number Question", () => {
test("Checkbox Group required and Number required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Number question (both required)",
secondQuestion: "number",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Checkbox Group and Number not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Number question (only checkbox required)",
secondQuestion: "number",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Checkbox Group Question and Radio group Question", () => {
test("Checkbox Group required and Radio group required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Radio question (both required)",
secondQuestion: "radio",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Checkbox Group and Radio group not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Radio question (only checkbox required)",
secondQuestion: "radio",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Checkbox Group Question and select Question", () => {
test("Checkbox Group required and select required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Select question (both required)",
secondQuestion: "select",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Checkbox Group and select not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("select", "select-test", "select test", false, "select test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Select question (only checkbox required)",
secondQuestion: "select",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Checkbox Group Question and Short text question", () => {
test("Checkbox Group required and Short Text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Text question (both required)",
secondQuestion: "text",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Checkbox Group and Short Text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "checkbox",
fillText: "Test Checkbox Group question and Text question (only checkbox required)",
secondQuestion: "text",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
});
});
@@ -1,54 +0,0 @@
import { test } from "../../lib/fixtures";
test.describe("Collective event type", () => {
const teamEventTitle = "Test Managed Event Type";
let userId: number;
test.beforeEach(async ({ page, users, bookingPage }) => {
const userFixture = await users.create(
{ name: "testuser" },
{ hasTeam: true, schedulingType: "COLLECTIVE", teamEventTitle }
);
await userFixture.apiLogin();
userId = userFixture.id;
await page.goto("/event-types");
await bookingPage.goToEventType(teamEventTitle);
});
test("Book a Collective event type", async ({ bookingPage }) => {
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillEmailAndName(eventTypePage);
await bookingPage.confirmBooking(eventTypePage);
await bookingPage.backToBookings(eventTypePage);
await bookingPage.assertLabelWithCorrectTeamName(eventTypePage, teamEventTitle);
await bookingPage.assertBookingWithCorrectTitleAndDescription(eventTypePage, {
profileName: "testuser",
bookingName: teamEventTitle,
teamName: `user-id-${userId}'s Team`,
});
await bookingPage.clickOnBooking(eventTypePage, teamEventTitle);
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBookingWithReason(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Book a Collective event type (with added guest)", async ({ bookingPage }) => {
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillEmailAndName(eventTypePage);
await bookingPage.addGuests(eventTypePage, { guests: ["test@example.com"] });
await bookingPage.confirmBooking(eventTypePage);
await bookingPage.backToBookings(eventTypePage);
await bookingPage.assertLabelWithCorrectTeamName(eventTypePage, teamEventTitle);
await bookingPage.assertBookingWithCorrectTitleAndDescription(eventTypePage, {
profileName: "testuser",
bookingName: teamEventTitle,
teamName: `user-id-${userId}'s Team`,
aditionalGuestEmail: "test@example.com",
});
await bookingPage.clickOnBooking(eventTypePage, teamEventTitle);
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBookingWithReason(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
@@ -1,483 +0,0 @@
import { loginUser } from "../fixtures/regularBookings";
import { test } from "../lib/fixtures";
test.describe("Booking With Long Text Question and Each Other Question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test.beforeEach(async ({ page, users }) => {
await loginUser(users);
await page.goto("/event-types");
});
test("Long Text and Address required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Address question (both required)",
secondQuestion: "address",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Long Text required and Address not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Address question (only Long Text required)",
secondQuestion: "address",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test.describe("Booking With Long Text Question and Checkbox Group Question", () => {
test("Long Text and Checkbox Group required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Checkbox Group question (both required)",
secondQuestion: "checkbox",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Long Text required and Checkbox Group not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Checkbox Group question (only Long Text required)",
secondQuestion: "checkbox",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Long Text Question and checkbox Question", () => {
test("Long Text and checkbox required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Checkbox question (only Long Text required)",
secondQuestion: "boolean",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Long Text required and checkbox not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Checkbox question (only Long Text required)",
secondQuestion: "boolean",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Long Text Question and Multiple email Question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test("Long Text and Multiple email required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Multiple email question (both required)",
secondQuestion: "multiemail",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Long Text required and Multiple email not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
false,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Multiple email question (only Long Text required)",
secondQuestion: "multiemail",
options: { hasPlaceholder: true, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Long Text Question and multiselect Question", () => {
test("Long Text and multiselect text required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and multiselect question (both required)",
secondQuestion: "multiselect",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Long Text required and multiselect text not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and multiselect question (only long text required)",
secondQuestion: "multiselect",
options: { hasPlaceholder: false, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Long Text Question and Number Question", () => {
test("Long Text and Number required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Number question (both required)",
secondQuestion: "multiselect",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test("Long Text required and Number not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Number question (only Long Textß required)",
secondQuestion: "multiselect",
options: { hasPlaceholder: false, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test.describe("Booking With Long Text Question and Phone Question", () => {
test("Long Text and Phone required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Phone question (both required)",
secondQuestion: "phone",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Long Text required and Phone not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Phone question (only Long Text required)",
secondQuestion: "phone",
options: { hasPlaceholder: false, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Long Text Question and Radio group Question", () => {
test("Long Text and Radio group required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Radio Group question (both required)",
secondQuestion: "radio",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Long Text required and Radio group not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Radio Group question (only Long Text required)",
secondQuestion: "radio",
options: { hasPlaceholder: false, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Long Text Question and select Question", () => {
test("Long Text and select required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("select", "select-test", "select test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Select question (both required)",
secondQuestion: "select",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Long Text required and select not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("select", "select-test", "select test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Select question (only Long Text required)",
secondQuestion: "select",
options: { hasPlaceholder: false, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Long Text Question and Short text question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test("Long Text and Short text required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Text question (both required)",
secondQuestion: "text",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Long Text required and Short text not required", async ({ bookingPage }) => {
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "textarea",
fillText: "Test Long Text question and Text question (only Long Text required)",
secondQuestion: "text",
options: { hasPlaceholder: false, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
});
@@ -1,553 +0,0 @@
import { loginUser } from "../fixtures/regularBookings";
import { test } from "../lib/fixtures";
test.describe("Booking With Multiple Email Question and Each Other Question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test.beforeEach(async ({ page, users, bookingPage }) => {
await loginUser(users);
await page.goto("/event-types");
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
});
test.describe("Booking With Multiple Email Question and Address Question", () => {
test("Multiple Email required and Address required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Address question (both required)",
secondQuestion: "address",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Multiple Email and Address not required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Address question (only Multiple Email required)",
secondQuestion: "address",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Multiple Email Question and checkbox group Question", () => {
test("Multiple Email required and checkbox group required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and checkbox group question (both required)",
secondQuestion: "checkbox",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Multiple Email and checkbox group not required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and checkbox group question (only Multiple Email required)",
secondQuestion: "checkbox",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Multiple Email Question and checkbox Question", () => {
test("Multiple Email required and checkbox required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and checkbox question (both required)",
secondQuestion: "boolean",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Multiple Email and checkbox not required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and checkbox (only Multiple Email required)",
secondQuestion: "boolean",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Multiple Email Question and Long text Question", () => {
test("Multiple Email required and Long text required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Long Text question (both required)",
secondQuestion: "textarea",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Multiple Email and Long text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Long Text question (only Multiple Email required)",
secondQuestion: "textarea",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Multiple Email Question and multiselect Question", () => {
test("Multiple Email required and multiselect text required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Multi Select question (both required)",
secondQuestion: "multiselect",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Multiple Email and multiselect text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Multi Select question (only Multiple Email required)",
secondQuestion: "multiselect",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Multiple Email Question and Number Question", () => {
test("Multiple Email required and Number required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Number question (both required)",
secondQuestion: "number",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Multiple Email and Number not required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Number question (only Multiple Email required)",
secondQuestion: "number",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Multiple email Question and phone Question", () => {
test("Multiple email required and Phone required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Phone question (both required)",
secondQuestion: "phone",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Multiple email and Phone not required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Phone question (both required)",
secondQuestion: "phone",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Multiple Email Question and Radio group Question", () => {
test("Multiple Email required and Radio group required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Radio question (both required)",
secondQuestion: "radio",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Multiple Email and Radio group not required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Radio question (only Multiple Email required)",
secondQuestion: "radio",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test.describe("Booking With Multiple Email Question and select Question", () => {
test("Multiple Email required and select required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Select question (both required)",
secondQuestion: "select",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Multiple Email and select not required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("select", "select-test", "select test", false, "select test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Select question (only Multiple Email required)",
secondQuestion: "select",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Multiple Email Question and Short text question", () => {
test("Multiple Email required and Short text required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Text question (both required)",
secondQuestion: "text",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Multiple Email and Short text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "multiemail",
fillText: "Test Multiple Email question and Text question (only Multiple Email required)",
secondQuestion: "text",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
});
});
@@ -1,446 +0,0 @@
import { loginUser } from "../fixtures/regularBookings";
import { test } from "../lib/fixtures";
test.describe("Booking With Phone Question and Each Other Question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test.beforeEach(async ({ page, users, bookingPage }) => {
await loginUser(users);
await page.goto("/event-types");
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
});
test.describe("Booking With Phone Question and Address Question", () => {
test("Phone and Address required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Address question (both required)",
secondQuestion: "address",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Phone required and Address not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Address question (only phone required)",
secondQuestion: "address",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test.describe("Booking With Phone Question and checkbox group Question", () => {
const bookingOptions = { hasPlaceholder: false, isRequired: true };
test("Phone and checkbox group required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and checkbox group question (both required)",
secondQuestion: "checkbox",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Phone required and checkbox group not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and checkbox group question (only phone required)",
secondQuestion: "checkbox",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Phone Question and checkbox Question", () => {
test("Phone and checkbox required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and checkbox question (both required)",
secondQuestion: "boolean",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Phone required and checkbox not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and checkbox (only phone required)",
secondQuestion: "boolean",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Phone Question and Long text Question", () => {
test("Phone and Long text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Long Text question (both required)",
secondQuestion: "textarea",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Phone required and Long text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Long Text question (only phone required)",
secondQuestion: "textarea",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Phone Question and Multi email Question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test("Phone and Multi email required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Multi Email question (both required)",
secondQuestion: "multiemail",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Phone required and Multi email not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
false,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Multi Email question (only phone required)",
secondQuestion: "multiemail",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Phone Question and multiselect Question", () => {
test("Phone and multiselect text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Multi Select question (both required)",
secondQuestion: "multiselect",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Phone required and multiselect text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Multi Select question (only phone required)",
secondQuestion: "multiselect",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Phone Question and Number Question", () => {
test("Phone and Number required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Number question (both required)",
secondQuestion: "number",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Phone required and Number not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Number question (only phone required)",
secondQuestion: "number",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Phone Question and Radio group Question", () => {
test("Phone and Radio group required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Radio question (both required)",
secondQuestion: "radio",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Phone required and Radio group not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Radio question (only phone required)",
secondQuestion: "radio",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Phone Question and select Question", () => {
test("Phone and select required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Select question (both required)",
secondQuestion: "select",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Phone required and select not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("select", "select-test", "select test", false, "select test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Select question (only phone required)",
secondQuestion: "select",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Phone Question and Short text question", () => {
test("Phone and Short text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Text question (both required)",
secondQuestion: "text",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Phone required and Short text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Text question (only phone required)",
secondQuestion: "text",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
});
});
@@ -1,444 +0,0 @@
import { loginUser } from "../fixtures/regularBookings";
import { test } from "../lib/fixtures";
test.describe("Booking With Radio Question and Each Other Question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test.beforeEach(async ({ page, users, bookingPage }) => {
await loginUser(users);
await page.goto("/event-types");
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
});
test.describe("Booking With Radio Question and Address Question", () => {
test("Radio required and Address required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Address question (both required)",
secondQuestion: "address",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Radio and Address not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Address question (only radio required)",
secondQuestion: "address",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test.describe("Booking With Radio Question and checkbox group Question", () => {
test("Radio required and checkbox group required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and checkbox group question (both required)",
secondQuestion: "checkbox",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Radio and checkbox group not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and checkbox group question (only radio required)",
secondQuestion: "checkbox",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Radio Question and checkbox Question", () => {
test("Radio required and checkbox required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and checkbox question (both required)",
secondQuestion: "boolean",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Radio and checkbox not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and checkbox (only radio required)",
secondQuestion: "boolean",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Radio Question and Long text Question", () => {
test("Radio required and Long text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Long Text question (both required)",
secondQuestion: "textarea",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Radio and Long text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Long Text question (only radio required)",
secondQuestion: "textarea",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Radio Question and Multi email Question", () => {
test("Radio required and Multi email required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Multi Email question (both required)",
secondQuestion: "multiemail",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Radio and Multi email not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
false,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Multi Email question (only radio required)",
secondQuestion: "multiemail",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Radio Question and multiselect Question", () => {
test("Radio required and multiselect text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Multi Select question (both required)",
secondQuestion: "multiselect",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Radio and multiselect text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Multi Select question (only radio required)",
secondQuestion: "multiselect",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Radio Question and Number Question", () => {
test("Radio required and Number required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Number question (both required)",
secondQuestion: "number",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Radio and Number not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Number question (only radio required)",
secondQuestion: "number",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Radio Question and Phone Question", () => {
test("Radio required and Phone required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Phone question (both required)",
secondQuestion: "phone",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Radio and Phone not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Phone question (only radio required)",
secondQuestion: "phone",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Radio Question and select Question", () => {
test("Radio required and select required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Select question (both required)",
secondQuestion: "select",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Radio and select not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("select", "select-test", "select test", false, "select test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Select question (only radio required)",
secondQuestion: "select",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Radio Question and Short text question", () => {
test("Radio required and Short text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Text question (both required)",
secondQuestion: "text",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Radio and Short text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "radio",
fillText: "Test Radio question and Text question (only radio required)",
secondQuestion: "text",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
});
});
@@ -1,28 +0,0 @@
/* eslint-disable playwright/no-conditional-in-test */
import { loginUser } from "../fixtures/regularBookings";
import { test } from "../lib/fixtures";
test.describe.configure({ mode: "serial" });
test.describe("Booking with recurring checked", () => {
test.beforeEach(async ({ page, users, bookingPage }) => {
await loginUser(users);
await page.goto("/event-types");
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("recurring");
});
test("Updates event type with recurring events", async ({ page, bookingPage }) => {
await bookingPage.updateRecurringTab("2", "3");
await bookingPage.updateEventType();
await page.getByRole("link", { name: "Event Types" }).click();
await bookingPage.assertRepeatEventType();
});
test("Updates and shows recurring schedule correctly in booking page", async ({ bookingPage }) => {
await bookingPage.updateRecurringTab("2", "3");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.fillRecurringFieldAndConfirm(eventTypePage);
});
});
@@ -1,107 +0,0 @@
/* eslint-disable playwright/no-conditional-in-test */
import { loginUser } from "../fixtures/regularBookings";
import { test } from "../lib/fixtures";
const resolutions = [
{
width: 1920,
height: 1080,
},
{
width: 1280,
height: 720,
},
{
width: 640,
height: 480,
},
];
test.describe("Booking With All Questions", () => {
test.beforeEach(async ({ page, users, bookingPage }) => {
await loginUser(users);
await page.goto("/event-types");
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
const allQuestions = [
"phone",
"address",
"checkbox",
"boolean",
"textarea",
"multiemail",
"multiselect",
"number",
"radio",
"select",
"text",
];
for (const question of allQuestions) {
if (
question !== "number" &&
question !== "select" &&
question !== "checkbox" &&
question !== "boolean" &&
question !== "multiselect" &&
question !== "radio"
) {
await bookingPage.addQuestion(
question,
`${question}-test`,
`${question} test`,
true,
`${question} test`
);
} else {
await bookingPage.addQuestion(question, `${question}-test`, `${question} test`, true);
}
await bookingPage.checkField(question);
}
});
for (const resolution of resolutions) {
test(`Booking with ${resolution.width}x${resolution.height} resolution and all questions`, async ({
bookingPage,
}) => {
await bookingPage.setResolution(resolution.width, resolution.height);
await bookingPage.updateEventType();
});
}
});
test.describe("Booking With no questions", () => {
test.beforeEach(async ({ page, users, bookingPage }) => {
await loginUser(users);
await page.goto("/event-types");
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
});
for (const resolution of resolutions) {
test(`Booking with ${resolution.width}x${resolution.height} resolution and no questions`, async ({
bookingPage,
}) => {
await bookingPage.setResolution(resolution.width, resolution.height);
await bookingPage.updateEventType();
});
}
});
test.describe("Booking page with no questions", () => {
test.beforeEach(async ({ page, users, bookingPage }) => {
await loginUser(users);
await page.goto("/event-types");
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
});
for (const resolution of resolutions) {
test(`Booking page with ${resolution.width}x${resolution.height} resolution`, async ({ bookingPage }) => {
const eventTypePage = await bookingPage.previewEventType();
await eventTypePage.setViewportSize({ width: resolution.width, height: resolution.height });
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.confirmBooking(eventTypePage);
});
}
});
@@ -1,441 +0,0 @@
import { loginUser } from "../fixtures/regularBookings";
import { test } from "../lib/fixtures";
test.describe("Booking With Phone Question and Each Other Question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test.beforeEach(async ({ page, users, bookingPage }) => {
await loginUser(users);
await page.goto("/event-types");
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
});
test.describe("Booking With Select Question and Address Question", () => {
test("Select required and Address required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and Address question (both required)",
secondQuestion: "address",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Address not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and Address question (only select required)",
secondQuestion: "address",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and checkbox group Question", () => {
test("Select required and checkbox group required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and Checkbox question (both required)",
secondQuestion: "checkbox",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and checkbox group not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and Checkbox question (only Select required)",
secondQuestion: "checkbox",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and checkbox Question", () => {
test("Select required and checkbox required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and boolean question (both required)",
secondQuestion: "boolean",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and checkbox not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and boolean question (only select required)",
secondQuestion: "boolean",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and Long text Question", () => {
test("Select required and Long text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and textarea question (both required)",
secondQuestion: "textarea",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Long text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and textarea question (only select required)",
secondQuestion: "textarea",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and Multi email Question", () => {
test("Select required and Multi email required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and multiemail question (both required)",
secondQuestion: "multiemail",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Multi email not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
false,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and multiemail question (only select required)",
secondQuestion: "multiemail",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and multiselect Question", () => {
test("Select required and multiselect text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and multiselect question (both required)",
secondQuestion: "multiselect",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and multiselect text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and multiselect question (only select required)",
secondQuestion: "multiselect",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and Number Question", () => {
test("Select required and Number required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and number question (both required)",
secondQuestion: "number",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Number not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and number question (only select required)",
secondQuestion: "number",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and Phone Question", () => {
test("Select required and select required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and phone question (both required)",
secondQuestion: "phone",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Phone not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and phone question (only select required)",
secondQuestion: "phone",
options: { ...bookingOptions, isRequired: false },
});
});
});
test.describe("Booking With Select Question and Radio group Question", () => {
test("Select required and Radio group required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and radio question (both required)",
secondQuestion: "radio",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Radio group not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and radio question (only select required)",
secondQuestion: "radio",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and Short text question", () => {
test("Select required and Short text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and text question (both required)",
secondQuestion: "text",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Short text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "select",
fillText: "Test Select question and text question (only select required)",
secondQuestion: "text",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
});
+29
View File
@@ -0,0 +1,29 @@
import { expect, type Page } from "@playwright/test";
export function createAppsFixture(page: Page) {
return {
goToAppsCategory: async (category: string) => {
await page.getByTestId(`app-store-category-${category}`).nth(1).click();
await page.goto("apps/categories/analytics");
},
installApp: async (app: string) => {
await page.getByTestId(`app-store-app-card-${app}`).click();
await page.getByTestId("install-app-button").click();
},
goBackToAppsPage: async () => {
await page.getByTestId("add-apps").click();
},
goToEventType: async (eventType: string) => {
await page.getByRole("link", { name: eventType }).click();
},
goToAppsTab: async () => {
await page.getByTestId("vertical-tab-apps").click();
},
activeApp: async (app: string) => {
await page.getByTestId(`${app}-app-switch`).click();
},
verifyAppsInfo: async (activeApps: number) => {
await expect(page.locator(`text=6 apps, ${activeApps} active`)).toBeVisible();
},
};
}
@@ -1,183 +1,14 @@
import { expect, type Page } from "@playwright/test";
import dayjs from "@calcom/dayjs";
import { WEBAPP_URL } from "@calcom/lib/constants";
import type { MembershipRole } from "@calcom/prisma/enums";
import { localize } from "../lib/testUtils";
import type { createUsersFixture } from "./users";
const reschedulePlaceholderText = "Let others know why you need to reschedule";
export const scheduleSuccessfullyText = "This meeting is scheduled";
const EMAIL = "test@test.com";
const EMAIL2 = "test2@test.com";
const PHONE = "+55 (32) 983289947";
type BookingOptions = {
hasPlaceholder?: boolean;
isReschedule?: boolean;
isRequired?: boolean;
isAllRequired?: boolean;
isMultiSelect?: boolean;
};
interface QuestionActions {
[key: string]: () => Promise<void>;
}
type teamBookingtypes = { isManagedType?: boolean; isRoundRobinType?: boolean; isCollectiveType?: boolean };
type customLocators = {
shouldChangeSelectLocator: boolean;
shouldUseLastRadioGroupLocator: boolean;
shouldUseFirstRadioGroupLocator: boolean;
shouldChangeMultiSelectLocator: boolean;
};
type fillAndConfirmBookingParams = {
eventTypePage: Page;
placeholderText: string;
question: string;
fillText: string;
secondQuestion: string;
options: BookingOptions;
};
type UserFixture = ReturnType<typeof createUsersFixture>;
function isLastDayOfMonth(): boolean {
const today = dayjs();
const endOfMonth = today.endOf("month");
return today.isSame(endOfMonth, "day");
}
const fillQuestion = async (eventTypePage: Page, questionType: string, customLocators: customLocators) => {
const questionActions: QuestionActions = {
phone: async () => {
await eventTypePage.locator('input[name="phone-test"]').clear();
await eventTypePage.locator('input[name="phone-test"]').fill(PHONE);
},
multiemail: async () => {
await eventTypePage.getByRole("button", { name: `${questionType} test` }).click();
await eventTypePage.getByPlaceholder(`${questionType} test`).fill(EMAIL);
await eventTypePage.getByTestId("add-another-guest").last().click();
await eventTypePage.getByPlaceholder(`${questionType} test`).last().fill(EMAIL2);
},
checkbox: async () => {
if (customLocators.shouldUseLastRadioGroupLocator || customLocators.shouldChangeMultiSelectLocator) {
await eventTypePage.getByLabel("Option 1").last().click();
await eventTypePage.getByLabel("Option 2").last().click();
} else if (customLocators.shouldUseFirstRadioGroupLocator) {
await eventTypePage.getByLabel("Option 1").first().click();
await eventTypePage.getByLabel("Option 2").first().click();
} else {
await eventTypePage.getByLabel("Option 1").click();
await eventTypePage.getByLabel("Option 2").click();
}
},
multiselect: async () => {
if (customLocators.shouldChangeMultiSelectLocator) {
await eventTypePage.getByLabel("multi-select-dropdown").click();
await eventTypePage.getByTestId("select-option-Option 1").click();
} else {
await eventTypePage.getByLabel("multi-select-dropdown").last().click();
await eventTypePage.getByTestId("select-option-Option 1").click();
}
},
boolean: async () => {
await eventTypePage.getByLabel(`${questionType} test`).check();
},
radio: async () => {
await eventTypePage.locator('[id="radio-test\\.option\\.0\\.radio"]').click();
},
select: async () => {
if (customLocators.shouldChangeSelectLocator) {
await eventTypePage.getByLabel("select-dropdown").first().click();
await eventTypePage.getByTestId("select-option-Option 1").click();
} else {
await eventTypePage.getByLabel("select-dropdown").last().click();
await eventTypePage.getByTestId("select-option-Option 1").click();
}
},
number: async () => {
await eventTypePage.getByPlaceholder(`${questionType} test`).click();
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("123");
},
address: async () => {
await eventTypePage.getByPlaceholder(`${questionType} test`).click();
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("address test");
},
textarea: async () => {
await eventTypePage.getByPlaceholder(`${questionType} test`).click();
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("textarea test");
},
text: async () => {
await eventTypePage.getByPlaceholder(`${questionType} test`).click();
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("text test");
},
};
if (questionActions[questionType]) {
await questionActions[questionType]();
}
};
const fillAllQuestions = async (eventTypePage: Page, questions: string[], options: BookingOptions) => {
if (options.isAllRequired) {
for (const question of questions) {
switch (question) {
case "email":
await eventTypePage.getByPlaceholder("Email").click();
await eventTypePage.getByPlaceholder("Email").fill(EMAIL);
break;
case "phone":
await eventTypePage.getByPlaceholder("Phone test").click();
await eventTypePage.getByPlaceholder("Phone test").fill(PHONE);
break;
case "address":
await eventTypePage.getByPlaceholder("Address test").click();
await eventTypePage.getByPlaceholder("Address test").fill("123 Main St, City, Country");
break;
case "textarea":
await eventTypePage.getByPlaceholder("Textarea test").click();
await eventTypePage.getByPlaceholder("Textarea test").fill("This is a sample text for textarea.");
break;
case "select":
await eventTypePage.getByLabel("select-dropdown").last().click();
await eventTypePage.getByTestId("select-option-Option 1").click();
break;
case "multiselect":
// select-dropdown
await eventTypePage.getByLabel("multi-select-dropdown").click();
await eventTypePage.getByTestId("select-option-Option 1").click();
break;
case "number":
await eventTypePage.getByLabel("number test").click();
await eventTypePage.getByLabel("number test").fill("123");
break;
case "radio":
await eventTypePage.getByRole("radiogroup").getByText("Option 1").check();
break;
case "text":
await eventTypePage.getByPlaceholder("Text test").click();
await eventTypePage.getByPlaceholder("Text test").fill("Sample text");
break;
case "checkbox":
await eventTypePage.getByLabel("Option 1").first().check();
await eventTypePage.getByLabel("Option 2").first().check();
break;
case "boolean":
await eventTypePage.getByLabel(`${question} test`).check();
break;
case "multiemail":
await eventTypePage.getByRole("button", { name: "multiemail test" }).click();
await eventTypePage.getByPlaceholder("multiemail test").fill(EMAIL);
break;
}
}
}
};
export async function loginUser(users: UserFixture) {
const pro = await users.create({ name: "testuser" });
await pro.apiLogin();
@@ -191,17 +22,6 @@ export async function loginUserWithTeam(users: UserFixture, role: MembershipRole
await pro.apiLogin();
}
const goToNextMonthIfNoAvailabilities = async (eventTypePage: Page) => {
try {
if (isLastDayOfMonth()) {
await eventTypePage.getByTestId("view_next_month").waitFor({ timeout: 6000 });
await eventTypePage.getByTestId("view_next_month").click();
}
} catch (err) {
console.info("No need to click on view next month button");
}
};
export function createBookingPageFixture(page: Page) {
return {
goToEventType: async (eventType: string) => {
@@ -216,308 +36,19 @@ export function createBookingPageFixture(page: Page) {
goToTab: async (tabName: string) => {
await page.getByTestId(`vertical-tab-${tabName}`).click();
},
goToEventTypesPage: async () => {
await page.goto("/event-types");
},
setResolution: async (width: number, height: number) => {
await page.setViewportSize({ width: width, height: height });
},
addQuestion: async (
questionType: string,
identifier: string,
label: string,
isRequired: boolean,
placeholder?: string
) => {
await page.getByTestId("add-field").click();
await page.getByTestId("test-field-type").click();
await page.getByTestId(`select-option-${questionType}`).click();
await page.getByLabel("Identifier").dblclick();
await page.getByLabel("Identifier").fill(identifier);
await page.getByLabel("Label").click();
await page.getByLabel("Label").fill(label);
if (placeholder) {
await page.getByLabel("Placeholder").click();
await page.getByLabel("Placeholder").fill(placeholder);
}
if (!isRequired) {
await page.getByRole("radio", { name: "No" }).click();
}
await page.getByTestId("field-add-save").click();
},
updateRecurringTab: async (repeatWeek: string, maxEvents: string) => {
const repeatText = (await localize("en"))("repeats_every");
const maximumOf = (await localize("en"))("for_a_maximum_of");
await page.getByTestId("recurring-event-check").click();
await page
.getByTestId("recurring-event-collapsible")
.locator("div")
.filter({ hasText: repeatText })
.getByRole("spinbutton")
.fill(repeatWeek);
await page
.getByTestId("recurring-event-collapsible")
.locator("div")
.filter({ hasText: maximumOf })
.getByRole("spinbutton")
.fill(maxEvents);
},
updateEventType: async () => {
await page.getByTestId("update-eventtype").click();
const toast = await page.waitForSelector('[data-testid="toast-success"]');
expect(toast).toBeTruthy();
},
previewEventType: async () => {
const eventtypePromise = page.waitForEvent("popup");
await page.getByTestId("preview-button").click();
return eventtypePromise;
},
selectTimeSlot: async (eventTypePage: Page) => {
await goToNextMonthIfNoAvailabilities(eventTypePage);
await eventTypePage.getByTestId("time").first().click();
},
clickReschedule: async () => {
await page.getByText("Reschedule").click();
},
selectFirstAvailableTime: async () => {
await page.getByTestId("time").first().click();
},
fillRescheduleReasonAndConfirm: async () => {
await page.getByPlaceholder(reschedulePlaceholderText).click();
await page.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule");
await page.getByTestId("confirm-reschedule-button").click();
},
fillRecurringFieldAndConfirm: async (eventTypePage: Page) => {
await eventTypePage.getByTestId("occurrence-input").click();
await eventTypePage.getByTestId("occurrence-input").fill("2");
await goToNextMonthIfNoAvailabilities(eventTypePage);
await eventTypePage.getByTestId("time").first().click();
await expect(eventTypePage.getByTestId("recurring-dates")).toBeVisible();
},
cancelBookingWithReason: async (page: Page) => {
await page.getByTestId("cancel").click();
await page.getByTestId("cancel_reason").fill("Test cancel");
await page.getByTestId("confirm_cancel").click();
},
assertBookingCanceled: async (page: Page) => {
await expect(page.getByTestId("cancelled-headline")).toBeVisible();
},
rescheduleBooking: async (eventTypePage: Page) => {
await goToNextMonthIfNoAvailabilities(eventTypePage);
await eventTypePage.getByText("Reschedule").click();
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
await eventTypePage.getByRole("button", { name: "View next" }).click();
}
await eventTypePage.getByTestId("time").first().click();
await eventTypePage.getByPlaceholder(reschedulePlaceholderText).click();
await eventTypePage.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule");
await eventTypePage.getByTestId("confirm-reschedule-button").click();
await eventTypePage.waitForTimeout(400);
if (
await eventTypePage.getByRole("heading", { name: "Could not reschedule the meeting." }).isVisible()
) {
await eventTypePage.getByTestId("back").click();
await eventTypePage.getByTestId("time").last().click();
await eventTypePage.getByTestId("confirm-reschedule-button").click();
}
},
assertBookingRescheduled: async (page: Page) => {
await expect(page.getByText(scheduleSuccessfullyText)).toBeVisible();
},
assertRepeatEventType: async () => {
await expect(page.getByTestId("repeat-eventtype")).toBeVisible();
},
cancelBooking: async (eventTypePage: Page) => {
await eventTypePage.getByTestId("cancel").click();
await eventTypePage.getByTestId("cancel_reason").fill("Test cancel");
await eventTypePage.getByTestId("confirm_cancel").click();
await expect(eventTypePage.getByTestId("cancelled-headline")).toBeVisible();
},
confirmBooking: async (eventTypePage: Page) => {
await eventTypePage.getByTestId("confirm-book-button").click();
await eventTypePage.waitForURL("booking/**");
await expect(eventTypePage.getByText(scheduleSuccessfullyText)).toBeVisible();
},
fillAndConfirmBooking: async ({
eventTypePage,
placeholderText,
question,
fillText,
secondQuestion,
options,
}: fillAndConfirmBookingParams) => {
const confirmButton = options.isReschedule ? "confirm-reschedule-button" : "confirm-book-button";
await expect(eventTypePage.getByText(`${secondQuestion} test`).first()).toBeVisible();
await eventTypePage.getByPlaceholder(placeholderText).fill(fillText);
// Change the selector for specifics cases related to select question
const shouldChangeSelectLocator = (question: string, secondQuestion: string): boolean =>
question === "select" && ["multiemail", "multiselect", "address"].includes(secondQuestion);
const shouldUseLastRadioGroupLocator = (question: string, secondQuestion: string): boolean =>
question === "radio" && secondQuestion === "checkbox";
const shouldUseFirstRadioGroupLocator = (question: string, secondQuestion: string): boolean =>
question === "checkbox" && secondQuestion === "radio";
const shouldChangeMultiSelectLocator = (question: string, secondQuestion: string): boolean =>
question === "multiselect" &&
["address", "checkbox", "multiemail", "select"].includes(secondQuestion);
const customLocators = {
shouldChangeSelectLocator: shouldChangeSelectLocator(question, secondQuestion),
shouldUseLastRadioGroupLocator: shouldUseLastRadioGroupLocator(question, secondQuestion),
shouldUseFirstRadioGroupLocator: shouldUseFirstRadioGroupLocator(question, secondQuestion),
shouldChangeMultiSelectLocator: shouldChangeMultiSelectLocator(question, secondQuestion),
};
// Fill the first question
await fillQuestion(eventTypePage, question, customLocators);
// Fill the second question if is required
options.isRequired && (await fillQuestion(eventTypePage, secondQuestion, customLocators));
await eventTypePage.getByTestId(confirmButton).click();
await eventTypePage.waitForTimeout(400);
if (await eventTypePage.getByRole("heading", { name: "Could not book the meeting." }).isVisible()) {
await eventTypePage.getByTestId("back").click();
await eventTypePage.getByTestId("time").last().click();
await fillQuestion(eventTypePage, question, customLocators);
options.isRequired && (await fillQuestion(eventTypePage, secondQuestion, customLocators));
await eventTypePage.getByTestId(confirmButton).click();
}
const scheduleSuccessfullyPage = eventTypePage.getByText(scheduleSuccessfullyText);
await scheduleSuccessfullyPage.waitFor({ state: "visible" });
await expect(scheduleSuccessfullyPage).toBeVisible();
},
fillAllQuestions: async (eventTypePage: Page, questions: string[], options: BookingOptions) => {
const confirmButton = options.isReschedule ? "confirm-reschedule-button" : "confirm-book-button";
await fillAllQuestions(eventTypePage, questions, options);
await eventTypePage.getByTestId(confirmButton).click();
const scheduleSuccessfullyPage = eventTypePage.getByText(scheduleSuccessfullyText);
await scheduleSuccessfullyPage.waitFor({ state: "visible" });
await expect(scheduleSuccessfullyPage).toBeVisible();
},
addGuests: async (eventTypePage: Page, options: { guests: string[] }) => {
await eventTypePage.getByTestId("add-guests").click();
for (const guest of options.guests) {
await eventTypePage.getByPlaceholder("Email").fill(guest);
}
},
createTeam: async (name: string) => {
await page.getByTestId("teams-test").click();
await page.getByTestId("new-team-btn").click();
await page.getByPlaceholder("Acme Inc.").click();
await page.getByPlaceholder("Acme Inc.").fill(name);
await page.getByTestId("continue-button").click();
await page.getByTestId("publish-button").click();
await page.getByTestId("vertical-tab-Back").click();
},
createTeamEventType: async (name: string, options: teamBookingtypes) => {
await page.getByTestId("new-event-type").click();
await page.getByTestId("option-0").click();
// We first simulate to create a default event type to check if managed option is not available
const managedEventDescription = (await localize("en"))("managed_event_description");
const roundRobinEventDescription = (await localize("en"))("round_robin_description");
const collectiveEventDescription = (await localize("en"))("collective_description");
const quickChatText = (await localize("en"))("quick_chat");
await expect(page.locator("div").filter({ hasText: managedEventDescription })).toBeHidden();
await page.getByTestId("dialog-rejection").click();
await page.getByTestId("new-event-type").click();
await page.getByTestId("option-team-1").click();
await page.getByPlaceholder(quickChatText).fill(name);
if (options.isCollectiveType) {
await page
.locator("div")
.filter({ hasText: `Collective${collectiveEventDescription}` })
.getByRole("radio")
.first()
.click();
}
if (options.isRoundRobinType) {
await page
.locator("div")
.filter({ hasText: `Round Robin${roundRobinEventDescription}` })
.getByRole("radio")
.nth(1)
.click();
}
if (options.isManagedType) {
await page
.locator("div")
.filter({ hasText: `Managed Event${managedEventDescription}` })
.getByRole("radio")
.last()
.click();
const managedEventClarification = (await localize("en"))("managed_event_url_clarification");
await expect(page.getByText(managedEventClarification)).toBeVisible();
}
const continueText = (await localize("en"))("continue");
const toast = page.getByTestId("toast-success");
await page.getByRole("button", { name: continueText }).click();
expect(toast).toBeTruthy();
await page.getByTestId("update-eventtype").click();
},
assertLabelWithCorrectTeamName: async (page: Page, teamName: string) => {
await expect(page.getByRole("link", { name: teamName }).first()).toBeVisible();
},
assertBookingWithCorrectTitleAndDescription: async (
page: Page,
options: { profileName: string; bookingName: string; teamName: string; aditionalGuestEmail?: string }
) => {
options.aditionalGuestEmail
? await expect(
page.getByRole("link", {
name: `${options.bookingName} between ${options.teamName} and ${options.profileName} You , ${options.profileName} and ${options.aditionalGuestEmail}`,
})
).toBeVisible()
: await expect(
page.getByRole("link", {
name: `${options.bookingName} between ${options.teamName} and ${options.profileName} You and ${options.profileName}`,
})
).toBeVisible();
},
clickOnBooking: async (page: Page, teamName: string) => {
await page.getByRole("link", { name: teamName }).first().click();
},
checkField: async (question: string, options?: { isOptional: boolean }) => {
if (options?.isOptional) {
await expect(page.getByTestId(`field-${question}-test`).getByTestId("optional")).toBeVisible();
} else {
await expect(page.getByTestId(`field-${question}-test`).getByTestId("required")).toBeVisible();
}
await expect(page.getByTestId(`field-${question}-test`)).toBeVisible();
},
checkRequiresConfirmation: async () => {
// Check existence of the icon
await expect(page.getByTestId("requires-confirmation-title").locator("svg")).toBeVisible();
@@ -526,7 +57,6 @@ export function createBookingPageFixture(page: Page) {
await expect(confirmationSwitch).toBeVisible();
await confirmationSwitch.click();
},
checkRequiresBookerEmailVerification: async () => {
await expect(page.getByTestId("requires-booker-email-verification-title").locator("svg")).toBeVisible();
@@ -535,7 +65,6 @@ export function createBookingPageFixture(page: Page) {
await expect(emailSwitch).toBeVisible();
await emailSwitch.click();
},
checkHideNotes: async () => {
await expect(page.getByTestId("disable-notes-title").locator("svg")).toBeVisible();
@@ -544,7 +73,6 @@ export function createBookingPageFixture(page: Page) {
await expect(hideNotesSwitch).toBeVisible();
await hideNotesSwitch.click();
},
checkRedirectOnBooking: async () => {
await expect(page.getByTestId("redirect-success-booking-title").locator("svg")).toBeVisible();
@@ -555,7 +83,6 @@ export function createBookingPageFixture(page: Page) {
await page.getByTestId("external-redirect-url").fill("https://cal.com");
await expect(page.getByTestId("redirect-url-warning")).toBeVisible();
},
checkEnablePrivateUrl: async () => {
await expect(page.getByTestId("hashedLinkCheck-title").locator("label div")).toBeVisible();
@@ -564,7 +91,6 @@ export function createBookingPageFixture(page: Page) {
await page.getByTestId("hashedLinkCheck").click();
await expect(page.getByTestId("generated-hash-url")).toBeVisible();
},
toggleOfferSeats: async () => {
await expect(page.getByTestId("offer-seats-toggle-title").locator("svg")).toBeVisible();
@@ -575,7 +101,6 @@ export function createBookingPageFixture(page: Page) {
await expect(seatSwitchField).toHaveValue("3");
await expect(page.getByTestId("show-attendees")).toBeVisible();
},
checkLockTimezone: async () => {
await expect(page.getByTestId("lock-timezone-toggle-title").locator("svg")).toBeVisible();
@@ -584,18 +109,9 @@ export function createBookingPageFixture(page: Page) {
await expect(lockSwitch).toBeVisible();
await lockSwitch.click();
},
checkEventType: async () => {
await expect(page.getByTestId("requires-confirmation-badge").last()).toBeVisible();
},
assertBookingIsVisible: async (name: string) => {
await expect(
page.getByRole("link", {
name,
})
).toBeVisible();
},
checkBufferTime: async () => {
const minutes = (await localize("en"))("minutes");
const fieldPlaceholder = page.getByPlaceholder("0");
@@ -629,7 +145,6 @@ export function createBookingPageFixture(page: Page) {
await page.getByTestId("select-option-60").click();
await expect(page.getByText(`60 ${minutes}`, { exact: true })).toBeVisible();
},
checkLimitBookingFrequency: async () => {
const fieldPlaceholder = page.getByPlaceholder("1").nth(1);
const limitFrequency = (await localize("en"))("limit_booking_frequency");
@@ -645,7 +160,6 @@ export function createBookingPageFixture(page: Page) {
await expect(fieldPlaceholder).toHaveValue("12");
await limitFrequencySwitch.click();
},
checkLimitBookingDuration: async () => {
const limitDuration = (await localize("en"))("limit_total_booking_duration");
const addlimit = (await localize("en"))("add_limit");
@@ -659,7 +173,6 @@ export function createBookingPageFixture(page: Page) {
await expect(page.getByTestId("add-limit")).toHaveCount(2);
await limitDurationSwitch.click();
},
checkLimitFutureBookings: async () => {
const limitFutureBookings = (await localize("en"))("limit_future_bookings");
const limitBookingsSwitch = page
@@ -672,7 +185,6 @@ export function createBookingPageFixture(page: Page) {
await expect(page.locator("#RANGE")).toBeChecked();
await limitBookingsSwitch.click();
},
checkOffsetTimes: async () => {
const offsetStart = (await localize("en"))("offset_start");
const offsetStartTimes = (await localize("en"))("offset_toggle");
@@ -685,54 +197,8 @@ export function createBookingPageFixture(page: Page) {
page.getByText("e.g. this will show time slots to your bookers at 9:10 AM instead of 9:00 AM")
).toBeVisible();
},
checkTimeSlotsCount: async (eventTypePage: Page, count: number) => {
await expect(eventTypePage.getByTestId("time")).toHaveCount(count);
},
createBookingWebhook: async (webhookReceiver: { url: string }, eventTitle: string) => {
const events = [
"Booking Canceled",
"Booking Created",
"Booking Rejected",
"Booking Requested",
"Booking Payment Initiated",
"Booking Rescheduled",
"Meeting Ended",
"Booking Paid",
"Recording Download Link Ready",
];
await page.goto(`${WEBAPP_URL}/event-types`);
await page.getByText(eventTitle).click();
await page.getByTestId("vertical-tab-webhooks").click();
await page.getByTestId("new_webhook").click();
await page.getByLabel("Subscriber URL").fill(webhookReceiver.url);
await page.getByRole("button", { name: "Ping test" }).click();
page
.getByTestId("dialog-creation")
.locator("div")
.filter({ hasText: '{ "ok": true, "status": 200, "message": "{}" }' })
.nth(2);
await page.getByRole("button", { name: "Create Webhook" }).click();
page.getByText(webhookReceiver.url);
events.forEach((event) => {
page.getByText(event);
});
},
rejectFirstBooking: async () => {
await page.goto("/bookings/unconfirmed");
await page.getByTestId("horizontal-tab-unconfirmed").click();
await page.getByTestId("booking-item").first().getByText("Reject").click();
await page.getByTestId("rejection-confirm").click();
},
fillEmailAndName: async (eventTypePage: Page) => {
const emailLabel = (await localize("en"))("email_address");
const nameLabel = (await localize("en"))("your_name");
await eventTypePage.getByLabel(emailLabel).fill(EMAIL);
await eventTypePage.getByLabel(nameLabel).fill("testuser");
},
};
}
+6
View File
@@ -4,6 +4,7 @@ import { test as base } from "@playwright/test";
import prisma from "@calcom/prisma";
import type { ExpectedUrlDetails } from "../../../../playwright.config";
import { createAppsFixture } from "../fixtures/apps";
import { createBookingsFixture } from "../fixtures/bookings";
import { createEmailsFixture } from "../fixtures/emails";
import { createEmbedsFixture } from "../fixtures/embeds";
@@ -32,6 +33,7 @@ export interface Fixtures {
workflowPage: ReturnType<typeof createWorkflowPageFixture>;
features: ReturnType<typeof createFeatureFixture>;
eventTypePage: ReturnType<typeof createEventTypeFixture>;
appsPage: ReturnType<typeof createAppsFixture>;
}
declare global {
@@ -104,4 +106,8 @@ export const test = base.extend<Fixtures>({
const eventTypePage = createEventTypeFixture(page);
await use(eventTypePage);
},
appsPage: async ({ page }, use) => {
const appsPage = createAppsFixture(page);
await use(appsPage);
},
});
+9 -13
View File
@@ -58,7 +58,7 @@ test.describe("Update Profile", () => {
await page.getByTestId("profile-update-email-submit-button").click();
const toastLocator = await page.waitForSelector('[data-testId="toast-success"]');
const toastLocator = await page.getByTestId("toast-success");
const textContent = await toastLocator.textContent();
@@ -84,7 +84,7 @@ test.describe("Update Profile", () => {
await page.goto(verifyUrl);
const errorLocator = await page.waitForSelector('[data-testId="toast-error"]');
const errorLocator = await page.getByTestId("toast-error");
expect(errorLocator).toBeDefined();
@@ -118,11 +118,7 @@ test.describe("Update Profile", () => {
await page.getByTestId("profile-update-email-submit-button").click();
const toastLocator = await page.waitForSelector('[data-testId="toast-success"]');
const textContent = await toastLocator.textContent();
expect(textContent).toContain(email);
expect(await page.getByTestId("toast-success").textContent()).toContain(email);
// Instead of dealing with emails in e2e lets just get the token and navigate to it
const verificationToken = await prisma.verificationToken.findFirst({
@@ -140,11 +136,13 @@ test.describe("Update Profile", () => {
await page.goto(verifyUrl);
const successLocator = await page.waitForSelector('[data-testId="toast-success"]');
expect(await page.getByTestId("toast-success").textContent()).toContain(email);
// After email verification is successfull. user is sent to /event-types
await page.waitForURL("/event-types");
expect(await successLocator.textContent()).toContain(email);
await page.goto("/settings/my-account/profile");
const emailInputUpdated = page.getByTestId("profile-form-email");
const emailInputUpdated = await page.getByTestId("profile-form-email");
expect(await emailInputUpdated.inputValue()).toEqual(email);
});
test("Can update a users email (verification disabled)", async ({ page, users, prisma, features }) => {
@@ -172,9 +170,7 @@ test.describe("Update Profile", () => {
await page.getByTestId("profile-update-email-submit-button").click();
const toastLocator = await page.waitForSelector('[data-testId="toast-success"]');
expect(await toastLocator.isVisible()).toBe(true);
expect(await page.getByTestId("toast-success").isVisible()).toBe(true);
const emailInputUpdated = page.getByTestId("profile-form-email");
File diff suppressed because it is too large Load Diff
+166 -1
View File
@@ -13,10 +13,17 @@
"reset_password_subject": "{{appName}}: Anleitung zum Zurücksetzen des Passworts",
"verify_email_subject": "{{appName}}: Bestätigen Sie Ihr Konto",
"check_your_email": "Schauen Sie in Ihrem E-Mail-Postfach nach",
"old_email_address": "Alte E-Mail-Adresse",
"new_email_address": "Neue E-Mail-Adresse",
"verify_email_page_body": "Wir haben eine E-Mail an {{email}} gesendet. Es ist wichtig, Ihre E-Mail-Adresse zu bestätigen, um die Zustellbarkeit für E-Mails und Kalenderbenachrichtigungen von {{appName}} zu gewährleisten.",
"verify_email_banner_body": "Bestätigen Sie Ihre E-Mail-Adresse, um die Zustellbarkeit von E-Mails und Kalenderbenachrichtigungen zu gewährleisten",
"verify_email_email_header": "Bestätigen Sie Ihre E-Mail-Adresse",
"verify_email_email_button": "E-Mail bestätigen",
"verify_email_change_description": "Sie haben vor kurzem angefordert, die E-Mail-Adresse zu ändern, die Sie verwenden, um sich bei Ihrem {{appName}} Konto anzumelden. Bitte klicken Sie auf die Schaltfläche unten, um Ihre neue E-Mail-Adresse zu bestätigen.",
"verify_email_change_success_toast": "Ihre E-Mail wurde auf {{email}} aktualisiert",
"verify_email_change_failure_toast": "Fehler beim Aktualisieren der E-Mail.",
"change_of_email": "Bestätigen Sie Ihre neue {{appName}} E-Mail-Adresse",
"change_of_email_toast": "Wir haben einen Bestätigungslink an {{email}} gesendet. Wir werden Ihre E-Mail-Adresse aktualisieren, sobald Sie auf diesen Link klicken.",
"copy_somewhere_safe": "Speichern Sie diesen API-Schlüssel an einem sicheren Ort. Sie können ihn nicht erneut ansehen.",
"verify_email_email_body": "Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie unten auf die Schaltfläche klicken.",
"verify_email_by_code_email_body": "Bitte bestätigen Sie Ihre E-Mail-Adresse mit dem unten stehenden Code.",
@@ -56,6 +63,17 @@
"a_refund_failed": "Erstattung fehlgeschlagen",
"awaiting_payment_subject": "Warte auf Zahlung: {{title}} am {{date}}",
"meeting_awaiting_payment": "Zahlung für den Termin noch offen",
"dark_theme_contrast_error": "Die Farbe des dunklen Designs hat die Kontrastprüfung nicht bestanden. Wir empfehlen Ihnen, diese Farbe zu ändern, damit Ihre Schaltflächen sichtbarer sind.",
"light_theme_contrast_error": "Die Farbe des hellen Designs hat die Kontrastprüfung nicht bestanden. Wir empfehlen Ihnen, diese Farbe zu ändern, damit Ihre Schaltflächen sichtbarer sind.",
"payment_not_created_error": "Zahlung konnte nicht erstellt werden",
"couldnt_charge_card_error": "Karte konnte nicht für die Zahlung belastet werden",
"no_available_users_found_error": "Keine verfügbaren Benutzer gefunden. Können Sie es mit einem anderen Zeitfenster versuchen?",
"request_body_end_time_internal_error": "Interner Fehler. Request-Body enthält keinen Endzeitpunkt",
"create_calendar_event_error": "Kalenderereignis konnte nicht im Kalender des Organisators erstellt werden",
"update_calendar_event_error": "Kalenderereignis konnte nicht aktualisiert werden.",
"delete_calendar_event_error": "Kalenderereignis konnte nicht gelöscht werden.",
"already_signed_up_for_this_booking_error": "Sie sind bereits für diese Buchung registriert.",
"hosts_unavailable_for_booking": "Einige der Veranstalter sind nicht für die Buchung verfügbar.",
"help": "Hilfe",
"price": "Preis",
"paid": "Bezahlt",
@@ -101,6 +119,7 @@
"requested_to_reschedule_subject_attendee": "Aktion erforderlich - Neuplanung: Bitte buchen Sie eine neue Zeit für „{{eventType}}“ mit {{name}}",
"hi_user_name": "Hallo, {{name}}",
"ics_event_title": "{{eventType}} mit {{name}}",
"please_book_a_time_sometime_later": "Ups, wir konnten Sie dieses Mal nicht verbinden. Bitte planen Sie stattdessen einen zukünftigen Anruf",
"new_event_subject": "Neuer Termin: {{attendeeName}} - {{date}} - {{eventType}}",
"join_by_entrypoint": "{{entryPoint}} beitreten",
"notes": "Notizen",
@@ -117,15 +136,20 @@
"meeting_id": "Termin-ID",
"meeting_password": "Termin-Passwort",
"meeting_url": "Termin-URL",
"meeting_url_not_found": "Meeting-URL nicht gefunden",
"token_not_found": "Token nicht gefunden",
"some_other_host_already_accepted_the_meeting": "Ein anderer Veranstalter hat das Meeting bereits akzeptiert. Möchten Sie trotzdem beitreten? <1>Mit dem Meeting fortfahren</1>",
"meeting_request_rejected": "Ihre Terminanfrage wurde abgelehnt",
"rejected_event_type_with_organizer": "Abgelehnt: {{eventType}} mit {{organizer}} auf {{date}}",
"hi": "Hi",
"join_team": "Team beitreten",
"manage_this_team": "Dieses Team verwalten",
"team_info": "Informationen zum Team",
"join_meeting": "Meeting beitreten",
"request_another_invitation_email": "Falls Sie {{toEmail}} lieber nicht als Ihre {{appName}}-E-Mail verwenden möchten oder bereits über ein {{appName}}-Konto verfügen, fordern Sie bitte eine weitere Einladung zu dieser E-Mail an.",
"you_have_been_invited": "Sie wurden eingeladen, dem Team {{teamName}} beizutreten",
"user_invited_you": "{{user}} hat Sie eingeladen, dem {{entity}} {{team}} auf {{appName}} beizutreten",
"user_invited_you_to_subteam": "{{user}} hat Sie eingeladen, dem Team {{team}} der Organisation {{parentTeamName}} auf {{appName}} beizutreten",
"hidden_team_member_title": "Sie sind in diesem Team ausgeblendet",
"hidden_team_member_message": "Ihre Teammitgliedschaft ist nicht bezahlt, entweder upgraden Sie Ihren Account auf Pro oder lassen den Team-Organisator wissen, dass er für Ihren Platz im Team bezahlen kann.",
"hidden_team_owner_message": "Sie benötigen einen Pro-Account um die Teamfunktionen zu verweden. Sie werden dem Team nicht angezeigt, bis der Account upgegradet wurde.",
@@ -229,6 +253,7 @@
"reset_your_password": "Legen Sie Ihr neues Passwort mit den Anweisungen fest, die an Ihre E-Mail-Adresse gesendet wurden.",
"email_change": "Melden Sie sich mit Ihrer neuen E-Mail-Adresse und Ihrem Passwort wieder an.",
"create_your_account": "Erstellen Sie Ihr Konto",
"create_your_calcom_account": "Erstellen Sie Ihr Cal.com-Konto",
"sign_up": "Registrieren",
"youve_been_logged_out": "Sie wurden abgemeldet",
"hope_to_see_you_soon": "Wir hoffen, Sie bald wieder zu sehen!",
@@ -266,6 +291,9 @@
"nearly_there_instructions": "Zu guter Letzt: Ein Foto und eine Kurzbeschreibung zu Ihnen helfen, öfter gebucht zu werden, da Kunden dann genauer wissen, bei wem sie einen Termin buchen.",
"set_availability_instructions": "Definieren Sie Zeiträume, wenn Sie wiederkehrend verfügbar sind. Sie können später weitere Zeiträume erstellen und verschiedenen Kalendern zuordnen.",
"set_availability": "Verfügbarkeit festlegen",
"set_availbility_description": "Legen Sie Zeitpläne für die Zeiten fest, in denen Sie gebucht werden möchten.",
"share_a_link_or_embed": "Link oder Einbettung teilen",
"share_a_link_or_embed_description": "Teilen Sie Ihren {{appName}} Link oder betten Sie ihn auf Ihrer Website ein.",
"availability_settings": "Verfügbarkeitseinstellungen",
"continue_without_calendar": "Ohne Kalender fortfahren",
"continue_with": "Mit {{appName}} fortfahren",
@@ -281,6 +309,7 @@
"welcome_to_calcom": "Willkommen bei {{appName}}",
"welcome_instructions": "Wie ist Ihr Name und in welcher Zeitzone sind Sie? Dies kann später bearbeitet werden.",
"connect_caldav": "Mit CalDav-Server verbinden",
"connect_ics_feed": "Mit einem ICS-Feed verbinden",
"connect": "Verbinden",
"try_for_free": "Kostenlos testen",
"create_booking_link_with_calcom": "Erstellen Sie Ihren eigenen Buchungslink mit {{appName}}",
@@ -419,6 +448,7 @@
"browse_api_documentation": "Durchsuchen Sie unsere API Dokumentation",
"leverage_our_api": "Nutzen Sie unsere API für die volle Kontrolle und Anpassbarkeit.",
"create_webhook": "Webhook erstellen",
"instant_meeting": "Sofortiges Meeting erstellt",
"booking_cancelled": "Termin abgesagt",
"booking_rescheduled": "Termin verlegt",
"recording_ready": "Download-Link der Aufnahme ist nun verfügbar",
@@ -533,6 +563,7 @@
"enter_number_between_range": "Bitte geben Sie eine Zahl zwischen 1 und {{maxOccurences}} ein",
"email_address": "E-Mail Adresse",
"enter_valid_email": "Bitte geben Sie eine gültige Emaildresse ein",
"please_schedule_future_call": " Bitte planen Sie einen Anruf in der Zukunft ein, falls wir nicht innerhalb von {{seconds}} Sekunden verfügbar sind",
"location": "Ort",
"address": "Adresse",
"enter_address": "Geben Sie eine Adresse ein",
@@ -592,6 +623,7 @@
"new_member": "Neues Mitglied",
"invite": "Einladen",
"add_team_members": "Teammitglieder hinzufügen",
"add_org_members": "Mitglieder hinzufügen",
"add_team_members_description": "Lade andere ein, deinem Team beizutreten",
"add_team_member": "Teammitglied hinzufügen",
"invite_new_member": "Ein neues Mitglied einladen",
@@ -606,8 +638,9 @@
"hide_book_a_team_member_description": "Blendet die „Ein Teammitglied buchen“-Schaltflächen auf Ihren öffentlichen Seiten aus.",
"danger_zone": "Achtung",
"account_deletion_cannot_be_undone": "Vorsicht. Löschen eines Kontos kann nicht rückgängig gemacht werden.",
"team_deletion_cannot_be_undone": "Seien Sie vorsichtig. Das Löschen eines Teams kann nicht rückgängig gemacht werden",
"back": "Zurück",
"cancel": "Absagen",
"cancel": "Abbrechen",
"cancel_all_remaining": "Alle verbleibenden absagen",
"apply": "Anwenden",
"cancel_event": "Termin absagen",
@@ -656,6 +689,7 @@
"default_duration": "Standardlaufzeit",
"default_duration_no_options": "Bitte wählen Sie zuerst die verfügbaren Laufzeiten aus",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minuten",
"round_robin": "Round Robin",
"round_robin_description": "Treffen zwischen mehreren Teammitgliedern durchwechseln.",
@@ -668,6 +702,7 @@
"add_members": "Mitglieder hinzufügen...",
"no_assigned_members": "Keine zugeordneten Mitglieder",
"assigned_to": "Zugeordnet zu",
"you_must_be_logged_in_to": "Sie müssen bei {{url}} angemeldet sein",
"start_assigning_members_above": "Mit der Zuordnung von Mitgliedern oben beginnen",
"locked_fields_admin_description": "Mitglieder werden dies nicht bearbeiten können",
"locked_fields_member_description": "Diese Option wurde vom Team-Admin gesperrt",
@@ -772,6 +807,7 @@
"additional_input_description": "Erfordert die Eingabe zusätzlicher Eingaben durch den Planer vor der Terminbestätigung",
"label": "Bezeichnung",
"placeholder": "Platzhalter",
"display_add_to_calendar_organizer": "„Zum Kalender hinzufügen“-E-Mail als Organisator anzeigen",
"type": "Typ",
"edit": "Bearbeiten",
"add_input": "Eingabe hinzufügen",
@@ -780,6 +816,9 @@
"requires_confirmation_description": "Die Buchung muss manuell bestätigt werden, bevor die Buchungsinformationen an die Integrationen weitergeleitet werden und eine Bestätigungsmail gesendet wird.",
"recurring_event": "Wiederkehrender Termin",
"recurring_event_description": "Personen können sich für wiederkehrende Termine anmelden",
"cannot_be_used_with_paid_event_types": "Es kann nicht mit bezahlten Ereignistypen verwendet werden",
"warning_payment_instant_meeting_event": "Softige Meetings werden von wiederkehrenden Ereignissen und Zahlungs-Apps noch nicht unterstützt",
"warning_instant_meeting_experimental": "Experimentell: „Sofortiges Meeting“-Ereignisse sind derzeit eine experimentelle Funktion.",
"starting": "Beginnen",
"disable_guests": "Gäste deaktivieren",
"disable_guests_description": "Das Hinzufügen zusätzlicher Gäste deaktivieren.",
@@ -788,6 +827,7 @@
"private_link_label": "Privater Link",
"private_link_hint": "Ihr privater Link wird nach jeder Nutzung neu generiert",
"copy_private_link": "Privaten Link kopieren",
"copy_private_link_to_event": "Privaten Link zum Ereignis kopieren",
"private_link_description": "Erzeugen Sie einen privaten Link, den Sie teilen können, ohne Ihren {{appName}}-Benutzername preiszugeben",
"invitees_can_schedule": "Teilnehmende können folgendes buchen",
"date_range": "Datumszeitraum",
@@ -847,6 +887,7 @@
"next_step": "Schritt überspringen",
"prev_step": "Vorheriger Schritt",
"install": "Installieren",
"start_paid_trial": "Kostenlose Testphase starten",
"installed": "Installiert",
"active_install_one": "{{count}} aktive Installation",
"active_install_other": "{{count}} aktive Installationen",
@@ -861,6 +902,7 @@
"toggle_calendars_conflict": "Wechseln Sie die Kalender in denen nach Konflikten gesucht werden sollen, um Doppelbuchungen zu vermeiden.",
"connect_additional_calendar": "Zusätzlichen Kalender verbinden",
"calendar_updated_successfully": "Kalender erfolgreich aktualisiert",
"check_here": "Hier auswählen",
"conferencing": "Videokonferenzen",
"calendar": "Kalender",
"payments": "Zahlungen",
@@ -1032,6 +1074,9 @@
"user_impersonation_heading": "Benutzeridentitätswechsel",
"user_impersonation_description": "Erlaubt es unserem Support-Team, sich vorübergehend mit Ihrem Konto anzumelden, um jegliche Probleme, die Sie gemeldet haben, schneller zu lösen.",
"team_impersonation_description": "Ermöglicht den Team-Administratoren, sich vorübergehend als Sie einzuloggen.",
"cal_signup_description": "Für Einzelpersonen kostenlos. Teampläne für kollaborative Funktionen.",
"make_org_private": "Organisation privat stellen",
"make_org_private_description": "Wenn diese Einstellung aktiv ist, können Ihre Organisationsmitglieder keine anderen Organisationsmitglieder sehen.",
"make_team_private": "Team privat stellen",
"make_team_private_description": "Wenn diese Einstellung aktiv ist, können deine Teammitglieder keine anderen Teammitglieder sehen.",
"you_cannot_see_team_members": "Sie können nicht alle Teammitglieder eines privaten Teams sehen.",
@@ -1091,6 +1136,7 @@
"developer_documentation": "Entwickler-Dokumentation",
"get_in_touch": "Kontakt aufnehmen",
"contact_support": "Support kontaktieren",
"premium_support": "Premium-Support",
"community_support": "Community Support",
"feedback": "Feedback",
"submitted_feedback": "Vielen Dank für Ihr Feedback!",
@@ -1254,6 +1300,7 @@
"default_calendar_selected": "Standardkalender",
"hide_from_profile": "Im Profil ausblenden",
"event_setup_tab_title": "Termin-Einrichtung",
"availability_not_found_in_schedule_error": "Keine Verfügbarkeit im Zeitplan gefunden",
"event_limit_tab_title": "Limits",
"event_limit_tab_description": "Wie oft können Sie gebucht werden",
"event_advanced_tab_description": "Kalendereinstellungen & mehr...",
@@ -1274,6 +1321,7 @@
"do_this": "Mache dies",
"turn_off": "Abschalten",
"turn_on": "Einschalten",
"cancelled_bookings_cannot_be_rescheduled": "Stornierte Buchungen können nicht verschoben werden",
"settings_updated_successfully": "Einstellungen erfolgreich aktualisiert",
"error_updating_settings": "Fehler beim Aktualisieren der Einstellungen",
"personal_cal_url": "Meine persönliche {{appName}}-URL",
@@ -1297,6 +1345,7 @@
"customize_your_brand_colors": "Passen Sie Ihre eigene Markenfarbe auf Ihrer Buchungsseite an.",
"pro": "Pro",
"removes_cal_branding": "Entfernt alle {{appName}}-bezogenen Markenzeichen, z. B. „Powered by {{appName}}.“",
"instant_meeting_with_title": "Sofortiges Meeting mit {{name}}",
"profile_picture": "Profilbild",
"upload": "Upload",
"add_profile_photo": "Profilbild hinzufügen",
@@ -1352,6 +1401,7 @@
"event_name_info": "Name des Ereignistyps",
"event_date_info": "Das Datum der Veranstaltung",
"event_time_info": "Die Startzeit des Termins",
"event_type_not_found": "Ereignistyp nicht gefunden",
"location_variable": "Ort",
"location_info": "Der Ort des Events",
"additional_notes_variable": "Zusätzliche Notizen",
@@ -1394,6 +1444,7 @@
"slot_length": "Zeitfensterlänge",
"booking_appearance": "Darstellung der Buchungsseite",
"appearance_team_description": "Einstellungen für die Buchungsdarstellung Ihres Teams verwalten",
"appearance_org_description": "Einstellungen für die Buchungsdarstellung Ihrer Organisation verwalten",
"only_owner_change": "Nur der Besitzer dieses Teams kann Änderungen an den Buchungen des Teams vornehmen ",
"team_disable_cal_branding_description": "Entfernt alle {{appName}}-bezogenen Markenzeichen, z. B. „Powered by {{appName}}“",
"invited_by_team": "{{teamName}} hat Sie eingeladen, dem Team als {{role}} beizutreten",
@@ -1447,6 +1498,7 @@
"password_updated": "Passwort aktualisiert!",
"pending_payment": "Zahlung ausstehend",
"pending_invites": "Offene Einladungen",
"pending_organization_invites": "Ausstehende Organisationseinladungen",
"not_on_cal": "Nicht auf {{appName}}",
"no_calendar_installed": "Kein Kalender installiert",
"no_calendar_installed_description": "Sie haben noch keinen Ihrer Kalender verbunden",
@@ -1458,6 +1510,8 @@
"report_app": "App melden",
"limit_booking_frequency": "Buchungsfrequenz begrenzen",
"limit_booking_frequency_description": "Begrenzen, wie oft dieses Ereignis gebucht werden kann",
"limit_booking_only_first_slot": "Buchun auf den ersten Platz beschränken",
"limit_booking_only_first_slot_description": "Nur die Buchung des ersten Platzes am Tag erlauben",
"limit_total_booking_duration": "Gesamtbuchungsdauer beschränken",
"limit_total_booking_duration_description": "Begrenzt die Gesamtanzahl, die dieser Termin gebucht werden kann",
"add_limit": "Begrenzung hinzufügen",
@@ -1525,10 +1579,14 @@
"your_org_disbanded_successfully": "Ihre Organization wurde erfolgreich aufgelöst",
"error_creating_team": "Fehler beim Erstellen des Teams",
"you": "Sie",
"or_continue_with": "Oder fortfahren mit",
"resend_email": "E-Mail erneut senden",
"member_already_invited": "Mitglied wurde bereits eingeladen",
"already_in_use_error": "Benutzername wird bereits verwendet",
"enter_email_or_username": "E-Mail oder Benutzername eingeben",
"enter_email": "E-Mail-Adresse eingeben",
"enter_emails": "E-Mail-Adressen eingeben",
"too_many_invites": "Sie sind darauf beschränkt, maximal {{nbUsers}} Benutzer gleichzeitig einzuladen.",
"team_name_taken": "Dieser Name ist bereits vergeben",
"must_enter_team_name": "Team-Name muss eingegeben werden",
"team_url_required": "Team-URL muss eingegeben werden",
@@ -1608,6 +1666,7 @@
"individual": "Person",
"all_bookings_filter_label": "Alle Buchungen",
"all_users_filter_label": "Alle Benutzer",
"all_event_types_filter_label": "Alle Ereignistypen",
"your_bookings_filter_label": "Ihre Buchungen",
"meeting_url_variable": "Meeting-URL",
"meeting_url_info": "Die URL der Meeting-Konferenz",
@@ -1704,9 +1763,11 @@
"organizer_timezone": "Zeitzone des Veranstalters",
"email_user_cta": "Einladung anzeigen",
"email_no_user_invite_heading_team": "Sie wurden eingeladen, einem {{appName}}-Team beizutreten",
"email_no_user_invite_heading_subteam": "Sie wurden eingeladen, einem Team der Organisation {{parentTeamName}} beizutreten",
"email_no_user_invite_heading_org": "Sie wurden eingeladen, einer {{appName}}-Organisation beizutreten",
"email_no_user_invite_subheading": "{{invitedBy}} hat Sie eingeladen, dem Team in {{appName}} beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne ständiges hin und her zu planen.",
"email_user_invite_subheading_team": "{{invitedBy}} hat Sie eingeladen, dem Team „{{teamName}}“ beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne Hin und Her zu planen.",
"email_user_invite_subheading_subteam": "{{invitedBy}} hat Sie eingeladen, dem Team „{{teamName}}“ in seiner bzw. ihrer Organisation {{parentTeamName}} bei {{appName}} beizutreten. {{appName}} ist der Terminplaner, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne Hin und Her zu planen.",
"email_user_invite_subheading_org": "{{invitedBy}} hat Sie eingeladen, ihrer Organisation „{{teamName}}“ in {{appName}} beizutreten. {{appName}} ist der Terminplaner, der es Ihnen und Ihrer Organisation ermöglicht, Meetings ohne ständiges hin und her zu planen.",
"email_no_user_invite_steps_intro": "Wir begleiten Sie durch ein paar Schritte, danach können Sie in kürzester Zeit stressfreie Planung mit Ihrer {{entity}} genießen.",
"email_no_user_step_one": "Wählen Sie Ihren Benutzernamen",
@@ -1839,7 +1900,9 @@
"review_event_type": "Ereignistyp überprüfen",
"looking_for_more_analytics": "Suchen Sie nach weiteren Analysedaten?",
"looking_for_more_insights": "Möchten Sie mehr Insights?",
"filters": "Filter",
"add_filter": "Filter hinzufügen",
"remove_filters": "Alle Filter löschen",
"select_user": "Benutzer auswählen",
"select_event_type": "Ereignistyp auswählen",
"select_date_range": "Datumsbereich auswählen",
@@ -1930,6 +1993,7 @@
"connect_google_workspace": "Google Workspace verbinden",
"google_workspace_admin_tooltip": "Sie müssen ein Workspace-Admin sein, um diese Funktion nutzen zu können",
"first_event_type_webhook_description": "Erstellen Sie Ihren ersten Webhook für diesen Termintypen",
"create_instant_meeting_webhook_description": "Erstellen Sie Ihren ersten Webhook mit „Sofortiges Meeting erstellt“ als Auslöser für diesen Ereignistyp",
"install_app_on": "App installieren auf",
"create_for": "Erstellen für",
"currency": "Währung",
@@ -1994,6 +2058,8 @@
"add_times_to_your_email": "Zeiten zur E-Mail hinzufügen",
"select_time": "Zeit auswählen",
"select_date": "Datum auswählen",
"connecting_you_to_someone": "Wir verbinden Sie mit jemandem.",
"please_do_not_close_this_tab": "Diesen Tab bitte nicht schließen",
"see_all_available_times": "Alle verfügbaren Zeiten ansehen",
"org_team_names_example_1": "z.B. Marketing-Team",
"org_team_names_example_2": "z.B. Vertriebsteam",
@@ -2011,8 +2077,12 @@
"description_requires_booker_email_verification": "Um sicherzustellen, dass die E-Mail des Buchenden vor der Terminplanung verifiziert ist",
"requires_confirmation_mandatory": "Textnachrichten können nur an Teilnehmer gesendet werden, wenn der Ereignistyp bestätigt werden muss.",
"organizations": "Organisationen",
"upload_cal_video_logo": "Cal Video Logo hochladen",
"update_cal_video_logo": "Cal Video Logo aktualisieren",
"cal_video_logo_upload_instruction": "Um sicherzustellen, dass Ihr Logo vor dem dunklen Hintergrund von Cal Video gut sichtbar ist, laden Sie bitte ein helles Bild im PNG- oder SVG-Format hoch, um die Transparenz zu erhalten.",
"org_admin_other_teams": "Weitere Teams",
"org_admin_other_teams_description": "Hier können Sie Teams innerhalb Ihrer Organisation sehen, zu denen Sie nicht gehören. Sie können sich bei Bedarf selbst hinzufügen.",
"not_part_of_org": "Sie gehören keiner Organisation an",
"no_other_teams_found": "Keine weiteren Teams gefunden",
"no_other_teams_found_description": "Es gibt keine weiteren Teams in dieser Organisation.",
"attendee_first_name_variable": "Vorname des Teilnehmers",
@@ -2048,7 +2118,9 @@
"org_error_processing": "Beim Verarbeiten dieser Organisation ist ein Fehler aufgetreten",
"orgs_page_description": "Eine Liste aller Organisationen. Wenn Sie eine Organisation akzeptieren, können sich alle Benutzer mit dieser E-Mail-Domain OHNE E-Mail-Überprüfung registrieren.",
"unverified": "Nicht verifiziert",
"verified": "Verifiziert",
"dns_missing": "DNS fehlt",
"dns_configured": "DNS konfiguriert",
"mark_dns_configured": "Als DNS konfiguriert kennzeichnen",
"value": "Wert",
"your_organization_updated_sucessfully": "Ihre Organisation wurde erfolgreich aktualisiert",
@@ -2058,8 +2130,11 @@
"oAuth": "OAuth",
"recently_added": "Kürzlich hinzugefügt",
"connect_all_calendars": "Verbinden Sie alle Ihre Kalender",
"connect_all_calendars_description": "{{appName}} liest die Verfügbarkeit aus allen Ihren vorhandenen Kalendern.",
"workflow_automation": "Workflow-Automatisierung",
"workflow_automation_description": "Personalisieren Sie Ihre Planungserfahrung mit Workflows",
"scheduling_for_your_team": "Workflow-Automatisierung",
"scheduling_for_your_team_description": "Planen Sie Ihr Team mit kollektiver Terminplanung und Round-Robin-Terminplanung",
"no_members_found": "Keine Mitglieder gefunden",
"event_setup_length_error": "Ereignis-Einrichtung: Die Dauer muss mindestens 1 Minute betragen.",
"availability_schedules": "Verfügbarkeitspläne",
@@ -2092,10 +2167,100 @@
"overlay_my_calendar": "Meinen Kalender überlagern",
"overlay_my_calendar_toc": "Durch das Verbinden mit Ihrem Kalender akzeptieren Sie unsere Datenschutzerklärung und Nutzungsbedingungen. Sie können den Zugriff jederzeit widerrufen.",
"view_overlay_calendar_events": "Sehen Sie sich Ihre Kalendertermine an, um Buchungskonflikte zu vermeiden.",
"join_event_location": "{{eventLocationType}} beitreten",
"troubleshooting": "Problembehandlung",
"calendars_were_checking_for_conflicts": "Kalender, die wir auf Konflikten überprüfen",
"availabilty_schedules": "Verfügbarkeitspläne",
"manage_calendars": "Kalender verwalten",
"manage_availability_schedules": "Verfügbarkeitspläne verwalten",
"lock_timezone_toggle_on_booking_page": "Zeitzone auf der Buchungsseite sperren",
"description_lock_timezone_toggle_on_booking_page": "Um die Zeitzone auf der Buchungsseite zu sperren, nützlich für Termine in Person.",
"event_setup_multiple_payment_apps_error": "Sie können nur eine Zahlungs-App pro Ereignistyp aktiv haben.",
"number_in_international_format": "Bitte geben Sie die Nummer im internationalen Format ein.",
"install_calendar": "Kalender installieren",
"branded_subdomain": "Marken-Subdomain",
"branded_subdomain_description": "Holen Sie sich Ihre eigene Marken-Subdomain, wie z.B. acme.cal.com",
"org_insights": "Organisationsweite Insights",
"org_insights_description": "Verstehen Sie, wie Ihre gesamte Organisation Zeit verbringt",
"extensive_whitelabeling": "Dedizierte Onboarding- und Engineeringsupport",
"extensive_whitelabeling_description": "Passen Sie Ihre Planungserfahrung mit Ihrem eigenen Logo, eigenen Farben und mehr an",
"unlimited_teams": "Unbegrenzte Teams",
"unlimited_teams_description": "Fügen Sie Ihrer Organisation so viele Unterteams hinzu, wie notwendig",
"unified_billing": "Einheitliche Abrechnung",
"unified_billing_description": "Fügen Sie eine einzige Kreditkarte hinzu, um alle Abos Ihres Teams zu bezahlen",
"advanced_managed_events": "Erweiterte verwaltete Ereignistypen",
"advanced_managed_events_description": "Fügen Sie eine einzige Kreditkarte hinzu, um alle Abonnements Ihres Teams zu bezahlen",
"enterprise_description": "Rüsten Sie auf Enterprise auf, um Ihre Organisation zu erstellen",
"create_your_org": "Erstellen Sie Ihre Organisation",
"create_your_org_description": "Rüsten Sie auf Enterprise auf und erhalten Sie eine Subdomain, einheitliche Abrechnung, Insights, umfangreiches Whitelabeling und mehr",
"other_payment_app_enabled": "Sie können nur eine Zahlungs-App pro Ereignistyp aktivieren",
"admin_delete_organization_description": "<ul><li>Teams, die Mitglieder dieser Organisation sind, werden ebenfalls zusammen mit ihren Ereignistypen gelöscht</li><li>Benutzer, die Teil der Organisation waren, werden nicht gelöscht und ihre Ereignistypen bleiben ebenfalls intakt.</li><li>Benutzernamen würden geändert werden, um zu ermöglichen, dass sie außerhalb der Organisation existieren können</li></ul>",
"admin_delete_organization_title": "{{organizationName}} löschen?",
"published": "Veröffentlicht",
"unpublished": "Unveröffentlicht",
"publish": "Veröffentlichen",
"org_publish_error": "Organisation konnte nicht veröffentlicht werden",
"troubleshooter_tooltip": "Öffnen Sie die Problembehandlung und finden Sie heraus, was mit Ihrem Zeitplan nicht stimmt",
"need_help": "Brauchst du Hilfe?",
"troubleshooter": "Problembehandlung",
"please_install_a_calendar": "Bitte installieren Sie einen Kalender",
"instant_tab_title": "Sofortige Buchung",
"instant_event_tab_description": "Personen sofort buchen lassen",
"uprade_to_create_instant_bookings": "Rüsten Sie auf Enterprise auf und lassen Sie Ihre Gäste an einem Sofortanruf teilnehmen, an dem die Teilnehmer direkt teilnehmen können. Dies ist nur für Team-Ereignistypen verfügbar",
"dont_want_to_wait": "Sie möchten nicht warten?",
"meeting_started": "Meeting gestartet",
"pay_and_book": "Zum Buchen bezahlen",
"booking_not_found_error": "Buchung konnte nicht gefunden werden",
"booking_seats_full_error": "Die Buchungsplätze sind voll",
"missing_payment_credential_error": "Fehlende Zahlungsinformationen",
"missing_payment_app_id_error": "Fehlende Zahlungsapp-ID",
"not_enough_available_seats_error": "Die Buchung hat nicht genügend Plätze verfügbar",
"user_redirect_title": "{{username}} ist derzeit kurzzeitig abwesend.",
"user_redirect_description": "In der Zwischenzeit wird {{profile.username}} für alle neuen geplanten Meetings im Namen von {{username}} verantwortlich sein.",
"out_of_office": "Abwesend",
"out_of_office_description": "Konfigurieren Sie Aktionen in Ihrem Profil während Sie abwesend sind.",
"send_request": "Anfrage senden",
"start_date_and_end_date_required": "Startdatum und Enddatum sind erforderlich",
"start_date_must_be_before_end_date": "Das Startdatum muss vor dem Enddatum liegen",
"start_date_must_be_in_the_future": "Das Startdatum muss in der Zukunft liegen",
"user_not_found": "Benutzer nicht gefunden",
"out_of_office_entry_already_exists": "Abwesenheitseintrag existiert bereits",
"out_of_office_id_required": "Abwesenheitseintrags-ID erforderlich",
"booking_redirect_infinite_not_allowed": "Es besteht bereits eine Buchungsumleitung von diesem Benutzer zu Ihnen.",
"success_entry_created": "Neuer Eintrag wurde erfolgreich erstellt",
"booking_redirect_email_subject": "Buchungsumleitungsbenachrichtigung",
"booking_redirect_email_title": "Buchungsumleitungsbenachrichtigung",
"booking_redirect_email_description": "Sie haben eine Umleitung der Buchung von {{toName}} erhalten, so dass deren Profillinks für das Zeitfenster an Sie umgeleitet werden: ",
"success_accept_booking_redirect": "Sie haben diese Buchungsumleitungsanfrage akzeptiert.",
"success_reject_booking_redirect": "Sie haben diese Buchungsumleitungsanfrage abgelehnt.",
"copy_link_booking_redirect_request": "Link zum Teilen der Anfrage kopieren",
"booking_redirect_request_title": "Buchungsumleitungsanfrage",
"select_team_member": "Teammitglied auswählen",
"going_away_title": "Verreisen Sie? Markieren Sie einfach Ihren Profillink für eine gewisse Zeit als nicht verfügbar.",
"redirect_team_enabled": "Leiten Sie Ihr Profil an ein anderes Teammitglied um",
"redirect_team_disabled": "Leiten Sie Ihr Profil an ein anderes Teammitglied um (Teamplan erforderlich)",
"out_of_office_unavailable_list": "Abwesenheits-Unverfügbarkeitsliste",
"success_deleted_entry_out_of_office": "Eintrag erfolgreich gelöscht",
"temporarily_out_of_office": "Temporär abwesend?",
"add_a_redirect": "Umleitung hinzufügen",
"create_entry": "Eintrag erstellen",
"time_range": "Zeitspanne",
"automatically_add_all_team_members": "Alle Teammitglieder hinzufügen, einschließlich zukünftiger Mitglieder",
"redirect_to": "Umleiten zu",
"having_trouble_finding_time": "Haben Sie Schwierigkeiten, ein Zeitfenster zu finden?",
"show_more": "Mehr anzeigen",
"assignment_description": "Planen Sie Meetings ein, wenn jeder verfügbar ist, oder rotieren Sie durch die Mitglieder Ihres Team",
"lowest": "Niedrigste",
"low": "Niedrig",
"medium": "Mittel",
"high": "Hoch",
"Highest": "Höchste",
"send_booker_to": "Bucher senden an",
"set_priority": "Priorität festlegen",
"priority_for_user": "Priorität von {{userName}}",
"change_priority": "Priorität ändern",
"field_identifiers_as_variables": "Verwenden Sie Feldbezeichner als Variablen für Ihre benutzerdefinierte Ereignisumleitung",
"field_identifiers_as_variables_with_example": "Verwenden Sie Feldbezeichner als Variablen für Ihre benutzerdefinierte Ereignisumleitung (z. B. {{variable}})",
"account_already_linked": "Konto ist bereits verknüpft",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -638,7 +638,7 @@
"hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.",
"danger_zone": "Danger zone",
"account_deletion_cannot_be_undone":"Be Careful. Account deletion cannot be undone.",
"team_deletion_cannot_be_undone":"Be Careful. Team deletion cannot be undone",
"team_deletion_cannot_be_undone":"Be Careful. Team deletion cannot be undone.",
"back": "Back",
"cancel": "Cancel",
"cancel_all_remaining": "Cancel all remaining",
@@ -808,6 +808,7 @@
"label": "Label",
"placeholder": "Placeholder",
"display_add_to_calendar_organizer":"Display 'Add to calendar' email as the organizer",
"reconnect_calendar_to_use": "Please note that you may need to disconnect and then reconnect your 'Add to Calendar' account to use this feature.",
"type": "Type",
"edit": "Edit",
"add_input": "Add an Input",
@@ -1472,8 +1473,10 @@
"route_to_the_right_person": "Route to the right person based on the answers to your form",
"form_description": "Create your form to route a booker",
"copy_link_to_form": "Copy link to form",
"theme": "Theme",
"theme": "Booking Page Theme",
"theme_applies_note": "This only applies to your public booking pages",
"app_theme": "Dashboard Theme",
"app_theme_applies_note": "This only applies to your logged in dashboard",
"theme_system": "System default",
"add_a_team": "Add a team",
"add_webhook_description": "Receive meeting data in real-time when something happens in {{appName}}",
@@ -893,7 +893,7 @@
"number_apps_other": "{{count}} aplicaciones",
"trending_apps": "Aplicaciones populares",
"most_popular": "Más popular",
"installed_apps": "Aplicaciones instaladas",
"installed_apps": "Apps instaladas",
"free_to_use_apps": "Gratis",
"no_category_apps": "No hay aplicaciones de {{category}}",
"all_apps": "Todas las aplicaciones",
@@ -13,10 +13,17 @@
"reset_password_subject": "{{appName}} : Instructions de réinitialisation de mot de passe",
"verify_email_subject": "{{appName}} : Vérifiez votre compte",
"check_your_email": "Vérifiez votre e-mail",
"old_email_address": "Ancienne adresse e-mail",
"new_email_address": "Nouvelle adresse e-mail",
"verify_email_page_body": "Nous avons envoyé un e-mail à {{email}}. Il est important de vérifier votre adresse e-mail pour garantir la meilleure délivrabilité des e-mails et des calendriers de {{appName}}.",
"verify_email_banner_body": "Vérifiez votre adresse e-mail pour garantir la meilleure délivrabilité des e-mails et des calendriers",
"verify_email_email_header": "Vérifiez votre adresse e-mail",
"verify_email_email_button": "Vérifier l'adresse e-mail",
"verify_email_change_description": "Vous avez récemment demandé à changer l'adresse e-mail que vous utilisez pour vous connecter à votre compte {{appName}}. Veuillez cliquer sur le bouton ci-dessous pour confirmer votre nouvelle adresse e-mail.",
"verify_email_change_success_toast": "Mise à jour de votre adresse e-mail. Nouvelle : {{email}}",
"verify_email_change_failure_toast": "Échec de la mise à jour de l'adresse e-mail.",
"change_of_email": "Faites vérifier votre nouvelle adresse e-mail {{appName}}",
"change_of_email_toast": "Nous avons envoyé un lien de vérification à {{email}}. Nous mettrons à jour votre adresse e-mail une fois que vous aurez cliqué sur ce lien.",
"copy_somewhere_safe": "Sauvegardez cette clé API dans un endroit sûr. Vous ne pourrez plus la revoir.",
"verify_email_email_body": "Veuillez vérifier votre adresse e-mail en cliquant sur le bouton ci-dessous.",
"verify_email_by_code_email_body": "Veuillez vérifier votre adresse e-mail en utilisant le code ci-dessous.",
@@ -56,6 +63,17 @@
"a_refund_failed": "Un remboursement a échoué",
"awaiting_payment_subject": "En attente de paiement : {{title}} le {{date}}",
"meeting_awaiting_payment": "Votre rendez-vous est en attente de paiement",
"dark_theme_contrast_error": "La couleur du thème sombre ne passe pas la vérification du contraste. Nous vous recommandons de modifier cette couleur afin que vos boutons soient plus visibles.",
"light_theme_contrast_error": "La couleur du thème clair ne passe pas la vérification du contraste. Nous vous recommandons de modifier cette couleur afin que vos boutons soient plus visibles.",
"payment_not_created_error": "Le paiement n'a pas pu être créé",
"couldnt_charge_card_error": "Impossible de débiter la carte pour le Paiement",
"no_available_users_found_error": "Aucun utilisateur disponible n'a été trouvé. Pourriez-vous essayer un autre créneau horaire ?",
"request_body_end_time_internal_error": "Erreur interne. Le corps de la demande ne contient pas l'heure de fin",
"create_calendar_event_error": "Impossible de créer un événement de Calendrier dans le calendrier de l'Organisateur",
"update_calendar_event_error": "Impossible de mettre à jour l'événement de Calendrier.",
"delete_calendar_event_error": "Impossible de supprimer l'événement de Calendrier.",
"already_signed_up_for_this_booking_error": "Vous êtes déjà inscrit(e) à cette réservation.",
"hosts_unavailable_for_booking": "Certains hôtes sont indisponibles pour la réservation.",
"help": "Aide",
"price": "Prix",
"paid": "Payé",
@@ -101,6 +119,7 @@
"requested_to_reschedule_subject_attendee": "Replanification requise : veuillez réserver un nouveau créneau pour {{eventType}} avec {{name}}",
"hi_user_name": "Bonjour {{name}}",
"ics_event_title": "{{eventType}} avec {{name}}",
"please_book_a_time_sometime_later": "Oups, nous n'avons pas pu vous connecter cette fois-ci. Veuillez plutôt planifier un appel ultérieur",
"new_event_subject": "Nouvel événement : {{attendeeName}} - {{date}} - {{eventType}}",
"join_by_entrypoint": "Rejoindre par {{entryPoint}}",
"notes": "Notes",
@@ -117,15 +136,20 @@
"meeting_id": "ID du rendez-vous",
"meeting_password": "Mot de passe du rendez-vous",
"meeting_url": "Lien du rendez-vous",
"meeting_url_not_found": "L'URL du rendez-vous est introuvable",
"token_not_found": "Jeton introuvable",
"some_other_host_already_accepted_the_meeting": "D'autres hôtes ont déjà accepté le rendez-vous. Voulez-vous quand même participer ? <1>Aller quand même dans Rendez-vous</1>",
"meeting_request_rejected": "Votre demande de rendez-vous a été refusée",
"rejected_event_type_with_organizer": "Refusé : {{eventType}} avec {{organizer}} le {{date}}",
"hi": "Bonjour",
"join_team": "Rejoindre l'équipe",
"manage_this_team": "Gérer cette équipe",
"team_info": "Informations sur l'équipe",
"join_meeting": "Rejoindre le rendez-vous",
"request_another_invitation_email": "Si vous préférez ne pas utiliser {{toEmail}} comme votre adresse e-mail {{appName}} ou si vous avez déjà un compte {{appName}}, veuillez demander une autre invitation à cette adresse.",
"you_have_been_invited": "Vous avez été invité à rejoindre l'équipe {{teamName}}",
"user_invited_you": "{{user}} vous a invité à rejoindre l'{{entity}} {{team}} sur {{appName}}",
"user_invited_you_to_subteam": "{{user}} vous a invité(e) à rejoindre l'équipe {{team}} de l'organisation {{parentTeamName}} sur {{appName}}",
"hidden_team_member_title": "Vous êtes masqué dans cette équipe",
"hidden_team_member_message": "Votre place n'est pas payée, passez au plan Pro ou faites savoir au propriétaire de l'équipe qu'il peut payer votre place.",
"hidden_team_owner_message": "Vous avez besoin d'un compte Pro pour utiliser les équipes, vous êtes masqué jusqu'à ce que vous mettiez à niveau.",
@@ -285,6 +309,7 @@
"welcome_to_calcom": "Bienvenue sur {{appName}}",
"welcome_instructions": "Dites-nous comment vous appeler et indiquez-nous dans quel fuseau horaire vous vous trouvez. Vous pourrez modifier cela plus tard. ",
"connect_caldav": "Se connecter au serveur CalDAV (Bêta)",
"connect_ics_feed": "Connexion à un flux ICS",
"connect": "Connecter",
"try_for_free": "Essayez gratuitement",
"create_booking_link_with_calcom": "Créez votre propre lien de réservation avec {{appName}}",
@@ -423,12 +448,14 @@
"browse_api_documentation": "Parcourez la documentation de l'API",
"leverage_our_api": "Profitez de notre API pour tout personnaliser et contrôler.",
"create_webhook": "Créer un webhook",
"instant_meeting": "Rendez-vous instantané créé",
"booking_cancelled": "Réservation annulée",
"booking_rescheduled": "Réservation replanifiée",
"recording_ready": "Lien de téléchargement de l'enregistrement prêt",
"booking_created": "Réservation créée",
"booking_rejected": "Réservation rejetée",
"booking_requested": "Réservation demandée",
"booking_payment_initiated": "Paiement de la réservation initiée",
"meeting_ended": "Rendez-vous terminé",
"form_submitted": "Formulaire envoyé",
"booking_paid": "Réservation payée",
@@ -536,6 +563,7 @@
"enter_number_between_range": "Veuillez entrer un nombre entre 1 et {{maxOccurences}}",
"email_address": "Adresse e-mail",
"enter_valid_email": "Veuillez saisir une adresse e-mail valide",
"please_schedule_future_call": " Veuillez programmer un appel ultérieur si nous ne sommes pas disponibles dans {{seconds}} secondes",
"location": "Lieu",
"address": "Adresse",
"enter_address": "Entrer une adresse",
@@ -595,6 +623,7 @@
"new_member": "Nouveau membre",
"invite": "Inviter",
"add_team_members": "Ajouter des membres d'équipe",
"add_org_members": "Ajouter des membres",
"add_team_members_description": "Invitez d'autres personnes à rejoindre votre équipe.",
"add_team_member": "Ajouter un membre d'équipe",
"invite_new_member": "Inviter un nouveau membre d'équipe",
@@ -609,6 +638,7 @@
"hide_book_a_team_member_description": "Masquez le bouton Réserver un membre d'équipe de vos pages publiques.",
"danger_zone": "Zone de danger",
"account_deletion_cannot_be_undone": "Attention, la suppression de compte est irréversible.",
"team_deletion_cannot_be_undone": "Attention. La suppression de l'équipe est irréversible",
"back": "Retour",
"cancel": "Annuler",
"cancel_all_remaining": "Annuler tous les événements restants",
@@ -672,6 +702,7 @@
"add_members": "Ajouter des membres...",
"no_assigned_members": "Aucun membre assigné",
"assigned_to": "Assigné à",
"you_must_be_logged_in_to": "Vous devez être connecté(e) à {{url}}",
"start_assigning_members_above": "Commencez à assigner des membres ci-dessus",
"locked_fields_admin_description": "Les membres ne pourront pas modifier ceci.",
"locked_fields_member_description": "Cette option a été verrouillée par l'administrateur de l'équipe.",
@@ -776,6 +807,7 @@
"additional_input_description": "Exiger que le planificateur saisisse des champs supplémentaires avant la confirmation de la réservation.",
"label": "Libellé",
"placeholder": "Texte de substitution",
"display_add_to_calendar_organizer": "Afficher l'e-mail « Ajouter au calendrier » en tant qu'organisateur",
"type": "Type",
"edit": "Modifier",
"add_input": "Ajouter un champ",
@@ -784,6 +816,9 @@
"requires_confirmation_description": "La réservation doit être confirmée manuellement avant d'être poussée vers les intégrations et qu'un e-mail de confirmation soit envoyé.",
"recurring_event": "Événement récurrent",
"recurring_event_description": "Les personnes peuvent s'inscrire à des événements récurrents.",
"cannot_be_used_with_paid_event_types": "À ne pas utiliser avec les types d'événement payants",
"warning_payment_instant_meeting_event": "Les Rendez-vous instantanés ne sont pas encore pris en charge par les événements récurrents et les applications de paiement",
"warning_instant_meeting_experimental": "Expérimental : Les événements de Rendez-vous instantané sont actuellement expérimentaux.",
"starting": "Début",
"disable_guests": "Désactiver les invités",
"disable_guests_description": "Désactivez l'ajout d'invités supplémentaires lors de la réservation.",
@@ -792,6 +827,7 @@
"private_link_label": "Lien privé",
"private_link_hint": "Votre lien privé sera régénéré après chaque utilisation",
"copy_private_link": "Copier le lien privé",
"copy_private_link_to_event": "Copier le lien privé de l'événement",
"private_link_description": "Générez un lien privé à partager sans révéler votre nom d'utilisateur {{appName}}.",
"invitees_can_schedule": "Les invités peuvent réserver",
"date_range": "Plage de dates",
@@ -866,6 +902,7 @@
"toggle_calendars_conflict": "Activez/désactivez les calendriers pour lesquels vous souhaitez vérifier les conflits afin d'éviter les doubles réservations.",
"connect_additional_calendar": "Connecter un calendrier supplémentaire",
"calendar_updated_successfully": "Calendrier mis à jour avec succès",
"check_here": "Vérifier ici",
"conferencing": "Visioconférence",
"calendar": "Calendrier",
"payments": "Paiements",
@@ -1038,6 +1075,8 @@
"user_impersonation_description": "Autorisez notre équipe d'assistance à se connecter temporairement à votre place pour nous aider à résoudre les problèmes que vous pourriez nous signaler.",
"team_impersonation_description": "Autorisez les propriétaires/administrateurs de votre équipe à se connecter temporairement à votre place.",
"cal_signup_description": "Gratuit pour les particuliers. Plans Équipes pour les fonctionnalités collaboratives.",
"make_org_private": "Rendre l'organisation privée",
"make_org_private_description": "Les membres de votre organisation ne pourront pas voir les autres membres de l'organisation lorsque cette option est activée.",
"make_team_private": "Rendre l'équipe privée",
"make_team_private_description": "Les membres de votre équipe ne pourront pas voir les autres membres de l'équipe si cette option est activée.",
"you_cannot_see_team_members": "Vous ne pouvez pas voir tous les membres d'une équipe privée.",
@@ -1222,6 +1261,7 @@
"organizer_name_variable": "Nom de l'organisateur",
"app_upgrade_description": "Pour pouvoir utiliser cette fonctionnalité, vous devez passer à un compte Pro.",
"invalid_number": "Numéro de téléphone invalide",
"invalid_url_error_message": "URL non valide pour {{label}}. Exemple d'URL : {{sampleUrl}}",
"navigate": "Naviguer",
"open": "Ouvrir",
"close": "Fermer",
@@ -1260,6 +1300,7 @@
"default_calendar_selected": "Calendrier par défaut",
"hide_from_profile": "Masquer du profil",
"event_setup_tab_title": "Configuration",
"availability_not_found_in_schedule_error": "Aucune disponibilité trouvée dans le planning",
"event_limit_tab_title": "Limites",
"event_limit_tab_description": "Fréquence de réservation",
"event_advanced_tab_description": "Paramètres du calendrier, etc.",
@@ -1280,6 +1321,7 @@
"do_this": "Effectuer ceci",
"turn_off": "Désactiver",
"turn_on": "Activer",
"cancelled_bookings_cannot_be_rescheduled": "Les réservations annulées ne peuvent pas être reprogrammées",
"settings_updated_successfully": "Paramètres mis à jour avec succès",
"error_updating_settings": "Erreur lors de la mise à jour des paramètres",
"personal_cal_url": "Mon lien {{appName}} personnel",
@@ -1296,12 +1338,14 @@
"select_calendars": "Sélectionnez les calendriers dont vous souhaitez vérifier les conflits afin d'éviter les doubles réservations.",
"check_for_conflicts": "Vérifier les conflits",
"view_recordings": "Voir les enregistrements",
"check_for_recordings": "Chercher des enregistrements",
"adding_events_to": "Ajout d'événements",
"follow_system_preferences": "Suivre les préférences système",
"custom_brand_colors": "Couleurs de marque personnalisées",
"customize_your_brand_colors": "Personnalisez la couleur de votre page de réservation à l'image de votre marque.",
"pro": "Pro",
"removes_cal_branding": "Supprimez toutes les références à la marque {{appName}}, par exemple « Propulsé par {{appName}} ».",
"instant_meeting_with_title": "Rendez-vous instantané avec {{name}}",
"profile_picture": "Photo de profil",
"upload": "Télécharger",
"add_profile_photo": "Ajouter une photo de profil",
@@ -1357,6 +1401,7 @@
"event_name_info": "Nom du type d'événement",
"event_date_info": "Date de l'événement",
"event_time_info": "Heure de début de l'événement",
"event_type_not_found": "Type d'événement introuvable",
"location_variable": "Lieu",
"location_info": "Lieu de l'événement",
"additional_notes_variable": "Notes supplémentaires",
@@ -1399,6 +1444,7 @@
"slot_length": "Durée du créneau",
"booking_appearance": "Apparence",
"appearance_team_description": "Gérez les paramètres d'apparence de vos réservations d'équipe.",
"appearance_org_description": "Gérez les paramètres d'apparence de vos réservations d'organisation",
"only_owner_change": "Seul le propriétaire de cette équipe peut apporter des modifications à la réservation de l'équipe ",
"team_disable_cal_branding_description": "Supprimez toutes les références à la marque {{appName}}, par exemple « Propulsé par {{appName}} ».",
"invited_by_team": "{{teamName}} vous a invité à rejoindre son équipe en tant que {{role}}",
@@ -1452,6 +1498,7 @@
"password_updated": "Mot de passe mis à jour !",
"pending_payment": "Paiement en attente",
"pending_invites": "Invitations en attente",
"pending_organization_invites": "Invitations de l'organisation en attente",
"not_on_cal": "Pas sur {{appName}}",
"no_calendar_installed": "Aucun calendrier installé",
"no_calendar_installed_description": "Vous n'avez pas encore connecté vos calendriers.",
@@ -1463,6 +1510,8 @@
"report_app": "Signaler l'application",
"limit_booking_frequency": "Limiter la fréquence de réservation",
"limit_booking_frequency_description": "Limitez le nombre de réservations possibles pour cet événement.",
"limit_booking_only_first_slot": "Limiter la réservation à un seul créneau",
"limit_booking_only_first_slot_description": "N'autoriser la réservation que du premier créneau de chaque jour",
"limit_total_booking_duration": "Limiter la durée totale de réservation",
"limit_total_booking_duration_description": "Limitez la durée totale pendant laquelle cet événement peut être réservé.",
"add_limit": "Ajouter une limite",
@@ -1535,6 +1584,9 @@
"member_already_invited": "Le membre a déjà été invité",
"already_in_use_error": "Nom d'utilisateur déjà utilisé",
"enter_email_or_username": "Saisissez une adresse e-mail ou un nom d'utilisateur",
"enter_email": "Saisissez une adresse e-mail",
"enter_emails": "Saisissez des adresses e-mail",
"too_many_invites": "Vous ne pouvez inviter qu'un maximum de {{nbUsers}} utilisateurs à la fois.",
"team_name_taken": "Ce nom est déjà pris",
"must_enter_team_name": "Vous devez saisir un nom d'équipe",
"team_url_required": "Vous devez saisir un lien d'équipe",
@@ -1614,6 +1666,7 @@
"individual": "Particulier",
"all_bookings_filter_label": "Toutes les réservations",
"all_users_filter_label": "Tous les utilisateurs",
"all_event_types_filter_label": "Tous les types d'événement",
"your_bookings_filter_label": "Vos réservations",
"meeting_url_variable": "Lien du rendez-vous",
"meeting_url_info": "Lien de la visioconférence du rendez-vous",
@@ -1628,6 +1681,7 @@
"date_overrides_mark_all_day_unavailable_other": "Marquer comme indisponible aux dates sélectionnées",
"date_overrides_add_btn": "Ajouter la superposition",
"date_overrides_update_btn": "Mettre à jour la superposition",
"date_successfully_added": "Ajout du remplacement de date réussi",
"event_type_duplicate_copy_text": "{{slug}}-copie",
"set_as_default": "Définir par défaut",
"hide_eventtype_details": "Masquer les détails du type d'événement",
@@ -1654,6 +1708,7 @@
"minimum_round_robin_hosts_count": "Nombre d'hôtes requis pour participer",
"hosts": "Hôtes",
"upgrade_to_enable_feature": "Vous devez créer une équipe pour activer cette fonctionnalité. Cliquez pour créer une équipe.",
"orgs_upgrade_to_enable_feature": "Vous devez passer sur notre formule entreprise pour activer cette fonctionnalité.",
"new_attendee": "Nouveau participant",
"awaiting_approval": "En attente de validation",
"requires_google_calendar": "Cette application nécessite une connexion à Google Agenda",
@@ -1708,9 +1763,11 @@
"organizer_timezone": "Fuseau horaire de l'organisateur",
"email_user_cta": "Voir l'invitation",
"email_no_user_invite_heading_team": "Vous avez été invité à rejoindre une équipe {{appName}}",
"email_no_user_invite_heading_subteam": "Vous avez été invité(e) à rejoindre une équipe de l'organisation {{parentTeamName}}",
"email_no_user_invite_heading_org": "Vous avez été invité à rejoindre une organisation {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son équipe sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
"email_user_invite_subheading_team": "{{invitedBy}} vous a invité à rejoindre son équipe « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
"email_user_invite_subheading_subteam": "{{invitedBy}} vous a invité(e) à rejoindre son équipe {{teamName}} dans son organisation {{parentTeamName}} sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
"email_user_invite_subheading_org": "{{invitedBy}} vous a invité à rejoindre son organisation « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre organisation d'organiser des rendez-vous sans échanges d'e-mails.",
"email_no_user_invite_steps_intro": "Nous vous guiderons à travers quelques étapes courtes et vous profiterez d'une planification sans stress avec votre {{entity}} en un rien de temps.",
"email_no_user_step_one": "Choisissez votre nom d'utilisateur",
@@ -1843,6 +1900,7 @@
"review_event_type": "Examiner le type d'événement",
"looking_for_more_analytics": "Vous cherchez plus d'analyses ?",
"looking_for_more_insights": "Vous cherchez plus de statistiques ?",
"filters": "Filtres",
"add_filter": "Ajouter un filtre",
"remove_filters": "Effacer les filtres",
"select_user": "Sélectionner un utilisateur",
@@ -1935,6 +1993,7 @@
"connect_google_workspace": "Connecter Google Workspace",
"google_workspace_admin_tooltip": "Vous devez être un administrateur d'espace de travail pour utiliser cette fonctionnalité",
"first_event_type_webhook_description": "Créez votre premier webhook pour ce type d'événement",
"create_instant_meeting_webhook_description": "Créez votre premier webhook avec « Rendez-vous instantané créé » comme déclencheur pour ce type d'événement",
"install_app_on": "Installer l’application sur",
"create_for": "Créer pour",
"currency": "Devise",
@@ -1999,6 +2058,8 @@
"add_times_to_your_email": "Sélectionnez quelques créneaux disponibles et intégrez-les dans votre e-mail.",
"select_time": "Sélectionner un créneau",
"select_date": "Sélectionner une date",
"connecting_you_to_someone": "Nous vous mettons en relation avec quelqu'un.",
"please_do_not_close_this_tab": "Veuillez ne pas fermer cet onglet",
"see_all_available_times": "Voir tous les créneaux disponibles",
"org_team_names_example_1": "p. ex. Équipe marketing",
"org_team_names_example_2": "p. ex. Équipe de vente",
@@ -2016,8 +2077,12 @@
"description_requires_booker_email_verification": "Pour assurer la vérification par e-mail du participant avant la planification des événements.",
"requires_confirmation_mandatory": "Les messages ne peuvent être envoyés aux participants que lorsque le type d'événement nécessite une confirmation.",
"organizations": "Organisations",
"upload_cal_video_logo": "Téléverser un logo vidéo Cal",
"update_cal_video_logo": "Mettre à jour un logo vidéo Cal",
"cal_video_logo_upload_instruction": "Pour assurer que votre logo reste visible sur le fond sombre de la vidéo Cal, veuillez téléverser une image de couleur claire au format PNG ou SVG afin de maintenir la transparence.",
"org_admin_other_teams": "Autres équipes",
"org_admin_other_teams_description": "Ici, vous pouvez voir les équipes au sein de votre organisation dont vous ne faites pas partie. Vous pouvez vous y ajouter si nécessaire.",
"not_part_of_org": "Vous ne faites partie d'aucune organisation",
"no_other_teams_found": "Aucune autre équipe trouvée",
"no_other_teams_found_description": "Il n'y a pas d'autres équipes dans cette organisation.",
"attendee_first_name_variable": "Prénom du participant",
@@ -2053,52 +2118,149 @@
"org_error_processing": "Une erreur s'est produite lors du traitement de cette organisation",
"orgs_page_description": "Une liste de toutes les organisations. Accepter une organisation permettra à tous les utilisateurs avec ce domaine de messagerie de s'inscrire SANS vérification d'e-mail.",
"unverified": "Non vérifié",
"verified": "Vérifié",
"dns_missing": "DNS manquant",
"dns_configured": "DNS configuré",
"mark_dns_configured": "Marquer comme DNS configuré",
"value": "Valeur",
"your_organization_updated_sucessfully": "Votre organisation a été mise à jour avec succès",
"team_no_event_types": "Cette équipe n'a aucun type d'événement",
"seat_options_doesnt_multiple_durations": "L'option par place ne prend pas en charge les durées multiples",
"include_calendar_event": "Inclure l'événement du calendrier",
"oAuth": "OAuth",
"recently_added": "Ajoutées récemment",
"connect_all_calendars": "Connectez vos calendriers",
"connect_all_calendars_description": "{{appName}} relève la disponibilité de tous vos calendriers existants.",
"workflow_automation": "Automatisation de workflows",
"workflow_automation_description": "Personnalisez votre expérience de planification avec les workflows",
"scheduling_for_your_team": "Automatisation de workflows",
"scheduling_for_your_team_description": "Planifiez pour votre équipe grâce à la planification collective et par permutation circulaire",
"no_members_found": "Aucun membre trouvé",
"event_setup_length_error": "Configuration de l'événement : la durée doit être d'au moins 1 minute.",
"availability_schedules": "Horaires de disponibilité",
"unauthorized": "Non autorisé",
"access_cal_account": "{{clientName}} aimerait accéder à votre compte {{appName}}",
"select_account_team": "Sélectionner un compte ou une équipe",
"allow_client_to": "Cela permettra au {{clientName}} de",
"associate_with_cal_account": "Vous associer à vos informations personnelles de {{clientName}}",
"see_personal_info": "Voir vos informations personnelles, y compris celles que vous avez rendues publiques",
"see_primary_email_address": "Voir votre adresse e-mail principale",
"connect_installed_apps": "Vous connecter à vos applications installées",
"access_event_type": "Lire, modifier, supprimer vos types d'événements",
"access_availability": "Lire, modifier, supprimer vos disponibilités",
"access_bookings": "Lire, modifier, supprimer vos réservations",
"allow_client_to_do": "Autoriser {{clientName}} à faire cela ?",
"oauth_access_information": "En cliquant sur autoriser, vous autorisez cette application à utiliser vos informations conformément à ses conditions de service et à sa politique de confidentialité. Vous pouvez supprimer l'accès dans l'App Store de {{appName}}.",
"allow": "Autoriser",
"view_only_edit_availability_not_onboarded": "Cet utilisateur n'a pas terminé son intégration. Vous ne pourrez pas définir sa disponibilité tant qu'il n'aura pas terminé son intégration.",
"view_only_edit_availability": "Vous consultez les disponibilités de cet utilisateur. Vous ne pouvez modifier que vos propres disponibilités.",
"you_can_override_calendar_in_advanced_tab": "Vous pouvez modifier ceci pour chaque événement dans les paramètres avancés de chaque type d'événement.",
"edit_users_availability": "Modifier la disponibilité de l'utilisateur : {{username}}",
"resend_invitation": "Renvoyer l'invitation",
"invitation_resent": "L'invitation a été renvoyée.",
"saml_sso": "SAML",
"add_client": "Ajouter un client",
"copy_client_secret_info": "Après avoir copié le secret, vous ne pourrez plus le voir",
"add_new_client": "Ajouter un nouveau client",
"this_app_is_not_setup_already": "Cette application n'a pas encore été configurée",
"as_csv": "au format CSV",
"overlay_my_calendar": "Superposer mon calendrier",
"overlay_my_calendar_toc": "En vous connectant à votre calendrier, vous acceptez notre politique de confidentialité et nos conditions d'utilisation. Vous pouvez révoquer cet accès à tout moment.",
"view_overlay_calendar_events": "Consultez les événements de votre calendrier afin d'éviter les réservations incompatibles.",
"join_event_location": "Rejoignez {{eventLocationType}}",
"troubleshooting": "Dépannage",
"calendars_were_checking_for_conflicts": "Calendriers dont nous vérifions les conflits",
"availabilty_schedules": "Horaires de disponibilité",
"manage_calendars": "Gérer les calendriers",
"manage_availability_schedules": "Gérer les horaires de disponibilité",
"lock_timezone_toggle_on_booking_page": "Verrouiller le fuseau horaire sur la page de réservation",
"description_lock_timezone_toggle_on_booking_page": "Pour verrouiller le fuseau horaire sur la page de réservation, utile pour les événements en personne.",
"event_setup_multiple_payment_apps_error": "Vous ne pouvez activer qu'une seule application de paiement par type d'événement.",
"number_in_international_format": "Veuillez entrer le numéro au format international.",
"install_calendar": "Installer Calendrier",
"branded_subdomain": "Sous-domaine de marque",
"branded_subdomain_description": "Obtenez votre propre sous-domaine de marque, tel que acme.cal.com",
"org_insights": "Statistiques à l'échelle de l'organisation",
"org_insights_description": "Comprenez comment l'ensemble de votre organisation passe son temps",
"extensive_whitelabeling": "Marque blanche étendue",
"extensive_whitelabeling_description": "Personnalisez votre expérience de planification avec votre propre logo, vos couleurs, etc.",
"unlimited_teams": "Équipes illimitées",
"unlimited_teams_description": "Ajoutez autant de sous-équipes que nécessaire à votre organisation",
"unified_billing": "Facturation unifiée",
"unified_billing_description": "Ajoutez une seule carte de crédit pour payer tous les abonnements de votre équipe",
"advanced_managed_events": "Types d'événements gérés avancés",
"advanced_managed_events_description": "Ajoutez une seule carte de crédit pour payer tous les abonnements de votre équipe",
"enterprise_description": "Passez en Entreprise pour créer votre organisation",
"create_your_org": "Créer votre organisation",
"create_your_org_description": "Passez en Entreprise et recevez un sous-domaine, une facturation unifiée, des statistiques, un marquage blanc extensif et plus encore",
"other_payment_app_enabled": "Vous ne pouvez activer qu'une seule application de paiement par type d'événement",
"admin_delete_organization_description": "<ul><li>Les équipes qui sont membres de cette organisation seront également supprimées, ainsi que leurs types d'événements</li><li>Les utilisateurs qui faisaient partie de l'organisation ne seront pas supprimés et leurs types d'événements resteront intacts.</li><li>Les noms d'utilisateur seront modifiés pour leur permettre d'exister en dehors de l'organisation</li></ul>",
"admin_delete_organization_title": "Supprimer {{organizationName}} ?",
"published": "Publiée",
"unpublished": "Non publiée",
"publish": "Publier",
"org_publish_error": "L'organisation n'a pas pu être publiée",
"troubleshooter_tooltip": "Ouvrez l'outil de dépannage et déterminez ce qui ne va pas avec votre planning",
"need_help": "Besoin d'aide ?",
"troubleshooter": "Dépannage",
"please_install_a_calendar": "Veuillez installer un publier",
"instant_tab_title": "Réservation instantanée",
"instant_event_tab_description": "Autoriser les gens à réserver immédiatement",
"uprade_to_create_instant_bookings": "Passez en Entreprise et autorisez les invités à rejoindre un appel instantané auquel les participants peuvent accéder directement. Ceci n'est valable que pour les types d'événements d'équipe",
"dont_want_to_wait": "Vous ne voulez pas attendre ?",
"meeting_started": "Le rendez-vous a commencé",
"pay_and_book": "Payer pour réserver",
"booking_not_found_error": "Impossible de trouver la réservation",
"booking_seats_full_error": "Les places de réservation sont complètes",
"missing_payment_credential_error": "Identifiants de paiement manquants",
"missing_payment_app_id_error": "Id de l'application de paiement manquant",
"not_enough_available_seats_error": "La réservation n'a pas assez de places disponibles",
"user_redirect_title": "{{username}} est actuellement en absence de courte durée.",
"user_redirect_description": "Pendant ce temps, {{profile.username}} sera responsable de tous les rendez-vous programmés de la part de {{username}}.",
"out_of_office": "Pas au bureau",
"out_of_office_description": "Configurez des actions dans votre profil pendant votre absence.",
"send_request": "Envoyer une demande",
"start_date_and_end_date_required": "Les dates de début et de fin sont requises",
"start_date_must_be_before_end_date": "La date de début doit être antérieure à la date de fin",
"start_date_must_be_in_the_future": "La date de début doit être dans le futur",
"user_not_found": "Utilisateur introuvable",
"out_of_office_entry_already_exists": "L'entrée Pas au bureau existe déjà",
"out_of_office_id_required": "L'id de l'entrée Pas au bureau est requis",
"booking_redirect_infinite_not_allowed": "Une redirection de réservation de cet utilisateur vers vous existe déjà.",
"success_entry_created": "Création d'une nouvelle entrée réussie",
"booking_redirect_email_subject": "Notification de redirection de réservation",
"booking_redirect_email_title": "Notification de redirection de réservation",
"booking_redirect_email_description": "Vous avez reçu une redirection de réservation de {{toName}} pour que les liens vers son profil soient redirigés vers vous pendant l'intervalle de temps : ",
"success_accept_booking_redirect": "Vous avez accepté cette demande de redirection de réservation.",
"success_reject_booking_redirect": "Vous avez refusé cette demande de redirection de réservation.",
"copy_link_booking_redirect_request": "Copier le lien pour partager la demande",
"booking_redirect_request_title": "Demande de redirection de réservation",
"select_team_member": "Sélectionner un membre de l'équipe",
"going_away_title": "Vous partez ? Marquez simplement le lien de votre profil comme étant indisponible pendant un certain temps.",
"redirect_team_enabled": "Redirigez votre profil vers un autre membre de l'équipe",
"redirect_team_disabled": "Redirigez votre profil vers un autre membre de l'équipe (formule Équipe requise)",
"out_of_office_unavailable_list": "Liste des indisponibilités Pas au bureau",
"success_deleted_entry_out_of_office": "Suppression de l'entrée réussie",
"temporarily_out_of_office": "Temporairement Pas au bureau ?",
"add_a_redirect": "Ajouter une redirection",
"create_entry": "Créer une entrée",
"time_range": "Plage temporelle",
"automatically_add_all_team_members": "Ajouter tous les membres de l'équipe, y compris les futurs membres",
"redirect_to": "Redirection vers",
"having_trouble_finding_time": "Vous avez du mal à trouver une heure ?",
"show_more": "En voir plus",
"assignment_description": "Planifiez des rendez-vous lorsque tout le monde est disponible ou faites tourner les membres de votre équipe",
"lowest": "le plus bas",
"low": "bas",
"medium": "moyen",
"high": "élevé",
"Highest": "le plus élevé",
"send_booker_to": "Envoyer un utilisateur vers",
"set_priority": "Définir la priorité",
"priority_for_user": "Priorité pour {{userName}}",
"change_priority": "changer la priorité",
"field_identifiers_as_variables": "Utiliser les identifiants de champs comme variables pour la redirection de votre événement personnalisé",
"field_identifiers_as_variables_with_example": "Utiliser les identifiants de champs comme variables pour la redirection de votre événement personnalisé (par ex. : {{variable}})",
"account_already_linked": "Le compte est déjà associé",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -807,6 +807,8 @@
"additional_input_description": "דרוש מהמתזמן להזין נתונים נוספים לפני אישור התזמון",
"label": "תווית",
"placeholder": "מציין מיקום",
"display_add_to_calendar_organizer": "הצגת דוא״ל ‚הוספה ללוח השנה’ כגוף מארגן",
"reconnect_calendar_to_use": "נא לשים לב שכנראה צריך להתנתק ואז להתחבר ללוח השנה שלך מחדש כדי להשתמש ביכולת הזאת.",
"type": "סוג",
"edit": "עריכה",
"add_input": "הוספת קלט",
@@ -1473,6 +1475,8 @@
"copy_link_to_form": "להעתיק את הקישור לטופס",
"theme": "ערכת נושא",
"theme_applies_note": "חלה רק על דפים של הזמנות ציבוריות",
"app_theme": "ערכת עיצוב ללוח בקרה",
"app_theme_applies_note": "חל רק על לוח הבקרה שנכנסת אליו",
"theme_system": "ברירת המחדל של המערכת",
"add_a_team": "הוספת צוות",
"add_webhook_description": "לקבל נתוני פגישה בזמן אמת כשמשהו קורה ב-{{appName}}",
@@ -1,4 +1,5 @@
{
"identity_provider": "Azonosítás szolgáltató",
"trial_days_left": "Még $t(day, {\"count\": {{days}} }) maradt a PRO próbaidőszakodból",
"day_one": "{{count}} nap",
"day_other": "{{count}} nap",
@@ -7,6 +8,7 @@
"upgrade_now": "Frissíts most",
"accept_invitation": "Meghívó elfogadása",
"have_any_questions": "Van kérdésed? Itt vagyunk hogy segítsünk.",
"verify_email_email_button": "Email igazolása",
"organizer": "Szervező",
"rejection_reason": "Lemondás oka",
"help": "Segítség",
@@ -160,7 +162,7 @@
"yearly_one": "év",
"yearly_other": "év",
"name": "Név",
"logo": "Logo",
"logo": "Logó",
"error": "Hiba",
"type": "Típus",
"edit": "Szerkesztés",
@@ -13,10 +13,17 @@
"reset_password_subject": "{{appName}}: istruzioni per reimpostare la password",
"verify_email_subject": "{{appName}}: verifica il tuo account",
"check_your_email": "Controlla la tua e-mail",
"old_email_address": "Vecchio indirizzo email",
"new_email_address": "Nuovo indirizzo email",
"verify_email_page_body": "Abbiamo inviato un'e-mail a {{email}}. È importante verificare il tuo indirizzo e-mail per garantire la consegna delle e-mail e delle notifiche del calendario {{appName}}.",
"verify_email_banner_body": "Verifica il tuo indirizzo e-mail per garantire la consegna delle e-mail e delle notifiche del calendario",
"verify_email_email_header": "Verifica il tuo indirizzo e-mail",
"verify_email_email_button": "Verifica l'e-mail",
"verify_email_change_description": "Hai recentemente chiesto di cambiare l'indirizzo email che utilizzi per accedere al tuo account {{appName}}. Fai clic sul pulsante qui sotto pe confermare il tuo nuovo indirizzo email.",
"verify_email_change_success_toast": "Indirizzo email aggiornato: {{email}}",
"verify_email_change_failure_toast": "Aggiornamento email non riuscito.",
"change_of_email": "Verifica il tuo nuovo indirizzo email per {{appName}}",
"change_of_email_toast": "Abbiamo inviato un link di verifica a {{email}}. Aggiorneremo il tuo indirizzo email quando farai clic sul link.",
"copy_somewhere_safe": "Salva questa chiave API in un posto sicuro. Non potrai visualizzarla in seguito.",
"verify_email_email_body": "Verifica il tuo indirizzo e-mail facendo clic sul pulsante qui sotto.",
"verify_email_by_code_email_body": "Verifica il tuo indirizzo e-mail utilizzando il codice sottostante.",
@@ -56,6 +63,17 @@
"a_refund_failed": "Un rimborso non è riuscito",
"awaiting_payment_subject": "In attesa del pagamento: {{title}} il {{date}}",
"meeting_awaiting_payment": "La tua riunione è in attesa di pagamento",
"dark_theme_contrast_error": "Il colore del tema scuro non supera il controllo del contrasto. Ti consigliamo di cambiare questo colore in modo che i pulsanti siano più visibili.",
"light_theme_contrast_error": "Il colore del tema chiaro non supera il controllo del contrasto. Ti consigliamo di cambiare questo colore in modo che i pulsanti siano più visibili.",
"payment_not_created_error": "Non è stato possibile creare il pagamento",
"couldnt_charge_card_error": "Non è stato possibile addebitare la carta per il pagamento",
"no_available_users_found_error": "Nessun utente disponibile. Puoi provare un altro intervallo temporale?",
"request_body_end_time_internal_error": "Errore interno. La richiesta non contiene l'ora finale",
"create_calendar_event_error": "Impossibile creare l'evento nel calendario dell'organizzatore",
"update_calendar_event_error": "Impossibile aggiornare l'evento nel calendario.",
"delete_calendar_event_error": "Impossibile eliminare l'evento dal calendario.",
"already_signed_up_for_this_booking_error": "Ti sei già registrato per questa prenotazione.",
"hosts_unavailable_for_booking": "Alcuni degli organizzatori non sono disponibili per la prenotazione.",
"help": "Guida",
"price": "Prezzo",
"paid": "Pagato",
@@ -101,6 +119,7 @@
"requested_to_reschedule_subject_attendee": "Azione richiesta per la riprogrammazione: prenota un nuovo orario per {{eventType}} con {{name}}",
"hi_user_name": "Ciao {{name}}",
"ics_event_title": "{{eventType}} con {{name}}",
"please_book_a_time_sometime_later": "Oops, non siamo riusciti a collegarti. Pianifica una chiamata futura",
"new_event_subject": "Richiesta di nuovo evento: {{attendeeName}} - {{date}} - {{eventType}}",
"join_by_entrypoint": "Entra da {{entryPoint}}",
"notes": "Osservazioni",
@@ -117,15 +136,20 @@
"meeting_id": "Meeting ID",
"meeting_password": "Password del meeting",
"meeting_url": "URL del Meeting",
"meeting_url_not_found": "URL della riunione non trovato",
"token_not_found": "Token non trovato",
"some_other_host_already_accepted_the_meeting": "Qualche altro host ha già accettato la riunione. Vuoi comunque partecipare? <1>Prosegui con la riunione</1>",
"meeting_request_rejected": "La tua richiesta di riunione è stata rifiutata",
"rejected_event_type_with_organizer": "Rifiutato: {{eventType}} con {{organizer}} il {{date}}",
"hi": "Ciao",
"join_team": "Unisciti al team",
"manage_this_team": "Gestisci questo team",
"team_info": "Informazioni sul team",
"join_meeting": "Partecipa alla riunione",
"request_another_invitation_email": "Se preferisci non usare {{toEmail}} come email {{appName}} o già hai un account {{appName}}, per favore richiedi un altro invito a quella email.",
"you_have_been_invited": "Sei stato invitato ad unirti al team {{teamName}}",
"user_invited_you": "{{user}} ti ha invitato a unirti a {{entity}} {{team}} su {{appName}}",
"user_invited_you_to_subteam": "{{user}} ti ha invitato a entrare nel team {{team}} dell'organizzazione {{parentTeamName}} su {{appName}}",
"hidden_team_member_title": "Sei nascosto in questo team",
"hidden_team_member_message": "Il tuo posto non è pagato. Passa a Pro oppure informa il proprietario del team che può pagare il tuo posto.",
"hidden_team_owner_message": "Per utilizzare i team ti occorre un account Pro. Sarai nascosto finché non cambierai piano.",
@@ -229,6 +253,7 @@
"reset_your_password": "Imposta la nuova password seguendo le istruzioni che ti sono state inviate via e-mail.",
"email_change": "Accedi di nuovo con il nuovo indirizzo e-mail e la nuova password.",
"create_your_account": "Crea il tuo account",
"create_your_calcom_account": "Crea il tuo accout Cal.com",
"sign_up": "Registrati",
"youve_been_logged_out": "Hai fatto il log out",
"hope_to_see_you_soon": "Speriamo di rivederti presto!",
@@ -266,6 +291,9 @@
"nearly_there_instructions": "Infine: qualche informazione su di te e una foto sono utili per ottenere prenotazioni e per far capire alle persone con chi stanno prenotando.",
"set_availability_instructions": "Definisci intervalli di tempo di quando sei disponibile su una base ricorrente. Puoi crearne di più in seguito e assegnarli a calendari diversi.",
"set_availability": "Imposta la tua disponibilità",
"set_availbility_description": "Programma gli orari in cui vuoi essere prenotato.",
"share_a_link_or_embed": "Condividi o pubblica un link",
"share_a_link_or_embed_description": "Condividi il tuo link {{appName}} o inseriscilo nel tuo sito.",
"availability_settings": "Impostazioni di disponibilità",
"continue_without_calendar": "Continua senza calendario",
"continue_with": "Continua con {{appName}}",
@@ -281,6 +309,7 @@
"welcome_to_calcom": "Benvenuto in {{appName}}",
"welcome_instructions": "Dimmi come chiamarti e facci sapere in che timezone sei. Potrai cambiarlo dopo.",
"connect_caldav": "Connetti al server CalDav",
"connect_ics_feed": "Collega a un feed ICS",
"connect": "Connetti",
"try_for_free": "Provalo gratuitamente",
"create_booking_link_with_calcom": "Crea il tuo link di prenotazione con {{appName}}",
@@ -419,6 +448,7 @@
"browse_api_documentation": "Sfoglia la nostra documentazione API",
"leverage_our_api": "Sfrutta la nostra API per il pieno controllo e la personalizzabilità.",
"create_webhook": "Crea Webhook",
"instant_meeting": "Riunione immediata creata",
"booking_cancelled": "Prenotazione cancellata",
"booking_rescheduled": "Prenotazione Riprogrammata",
"recording_ready": "Il link per scaricare la registrazione è pronto",
@@ -533,11 +563,13 @@
"enter_number_between_range": "Insrisci un numero compreso tra 1 e {{maxOccurences}}",
"email_address": "Indirizzo email",
"enter_valid_email": "Immettere un indirizzo e-mail valido",
"please_schedule_future_call": " Pianifica una chiamata futura se non siamo disponibili entro {{seconds}} secondi",
"location": "Luogo",
"address": "Indirizzo",
"enter_address": "Immettere l'indirizzo",
"in_person_attendee_address": "Di persona (indirizzo del partecipante)",
"yes": "Sì",
"no": "No",
"additional_notes": "Note aggiuntive",
"booking_fail": "Impossibile prenotare la riunione.",
"reschedule_fail": "Impossibile riprogrammare la riunione.",
@@ -591,6 +623,7 @@
"new_member": "Nuovo Membro",
"invite": "Invita",
"add_team_members": "Aggiungi membri del team",
"add_org_members": "Aggiungi membri",
"add_team_members_description": "Invita altri utenti a unirsi al tuo team",
"add_team_member": "Aggiungi un membro del team",
"invite_new_member": "Invita un nuovo membro",
@@ -605,6 +638,7 @@
"hide_book_a_team_member_description": "Nasconde il pulsante Prenota un membro del team nelle pagine pubbliche.",
"danger_zone": "Zona pericolosa",
"account_deletion_cannot_be_undone": "Attenzione. Non è possibile annullare l'eliminazione dell'account.",
"team_deletion_cannot_be_undone": "Attenzione. Non è possibile annullare l'eliminazione del team",
"back": "Indietro",
"cancel": "Annulla",
"cancel_all_remaining": "Annulla tutti i rimanenti",
@@ -655,6 +689,7 @@
"default_duration": "Durata predefinita",
"default_duration_no_options": "Scegliere prima delle durate disponibili",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minuti",
"round_robin": "Round Robin",
"round_robin_description": "Ciclo di riunioni tra più membri del team.",
@@ -667,6 +702,7 @@
"add_members": "Aggiungi membri...",
"no_assigned_members": "Nessun membro assegnato",
"assigned_to": "Assegnato a",
"you_must_be_logged_in_to": "Devi effettuare l'accesso a {{url}}",
"start_assigning_members_above": "Inizia ad assegnare membri in alto",
"locked_fields_admin_description": "I membri non potranno fare modifiche",
"locked_fields_member_description": "Questa opzione è stata bloccata dall'amministratore del team",
@@ -771,6 +807,7 @@
"additional_input_description": "Chiedi al creatore del programma di inserire dei campi di input aggiuntivi prima che la prenotazione sia confermata",
"label": "Etichetta",
"placeholder": "Segnaposto",
"display_add_to_calendar_organizer": "Visualizza l'email \"Aggiungi al calendario\" come organizzatore",
"type": "Tipo",
"edit": "Modifica",
"add_input": "Aggiungi un input",
@@ -779,6 +816,9 @@
"requires_confirmation_description": "La prenotazione deve essere confermata manualmente prima di essere inviata alle integrazioni e che venga inviata una e-mail di conferma.",
"recurring_event": "Evento ricorrente",
"recurring_event_description": "Le persone possono iscriversi a eventi ricorrenti",
"cannot_be_used_with_paid_event_types": "Non può essere utilizzato con i tipi di evento a pagamento",
"warning_payment_instant_meeting_event": "Le riunioni immediate non sono ancora disponibili con gli eventi ricorrenti e le app di pagamento",
"warning_instant_meeting_experimental": "Sperimentale: gli eventi Riunione immediata al momento sono in fase di sperimentazione.",
"starting": "Inizio",
"disable_guests": "Disabilita Ospiti",
"disable_guests_description": "Disabilita l'aggiunta di ulteriori ospiti durante la prenotazione.",
@@ -787,6 +827,7 @@
"private_link_label": "Link privato",
"private_link_hint": "Il tuo link privato verrà rigenerato dopo ogni utilizzo",
"copy_private_link": "Copia link privato",
"copy_private_link_to_event": "Copia link privato nell'evento",
"private_link_description": "Genera un URL privato da condividere senza mostrare il tuo nome utente {{appName}}",
"invitees_can_schedule": "Gli inviti possono programmare",
"date_range": "Range di date",
@@ -846,6 +887,7 @@
"next_step": "Salta passo",
"prev_step": "Passo precedente",
"install": "Installa",
"start_paid_trial": "Inizia la prova gratuita",
"installed": "Installato",
"active_install_one": "{{count}} installazione attiva",
"active_install_other": "{{count}} installazioni attive",
@@ -860,6 +902,7 @@
"toggle_calendars_conflict": "Attiva/disattiva i calendari in cui desideri controllare i conflitti per evitare doppie prenotazioni.",
"connect_additional_calendar": "Collega calendario aggiuntivo",
"calendar_updated_successfully": "Calendario aggiornato correttamente",
"check_here": "Controlla qui",
"conferencing": "Conferenze",
"calendar": "Calendario",
"payments": "Pagamenti",
@@ -1031,6 +1074,9 @@
"user_impersonation_heading": "Accesso con altra identità",
"user_impersonation_description": "Consente al nostro team di supporto di effettuare l'accesso temporaneo a tuo nome per risolvere rapidamente gli eventuali problemi da te segnalati.",
"team_impersonation_description": "Consente ai proprietari/amministratori del tuo team di accedere temporaneamente con il tuo nome.",
"cal_signup_description": "Gratuito per le singole pesone. Piani collettivi per le funzionalità di collaborazione.",
"make_org_private": "Rendi l'organizzazione privata",
"make_org_private_description": "Quando questa opzione è attivata, i membri della tua organizzazione non possono vedere gli altri membri.",
"make_team_private": "Rendi il team privato",
"make_team_private_description": "Quando questa opzione è attivata, i membri del tuo team non potranno vedere gli altri membri del team.",
"you_cannot_see_team_members": "Non è possibile vedere tutti i membri di un team privato.",
@@ -1090,6 +1136,7 @@
"developer_documentation": "Documentazione sviluppatore",
"get_in_touch": "Contattaci",
"contact_support": "Contatta il supporto",
"premium_support": "Assistenza Premium",
"community_support": "Supporto della community",
"feedback": "Feedback",
"submitted_feedback": "Grazie per il tuo feedback!",
@@ -1253,6 +1300,7 @@
"default_calendar_selected": "Calendario predefinito",
"hide_from_profile": "Nascondi nel profilo",
"event_setup_tab_title": "Impostazione evento",
"availability_not_found_in_schedule_error": "Nessuna disponibilità trovata nel programma",
"event_limit_tab_title": "Limiti",
"event_limit_tab_description": "Con quale frequenza è possibile fare una prenotazione",
"event_advanced_tab_description": "Impostazioni calendario e altro...",
@@ -1273,6 +1321,7 @@
"do_this": "Esegui questa operazione",
"turn_off": "Disattiva",
"turn_on": "Attiva",
"cancelled_bookings_cannot_be_rescheduled": "Le prenotazioni annullate non possono essere riprogrammate",
"settings_updated_successfully": "Impostazioni aggiornate correttamente",
"error_updating_settings": "Errore durante l'aggiornamento delle impostazioni",
"personal_cal_url": "Il mio URL personale di {{appName}}",
@@ -1296,6 +1345,7 @@
"customize_your_brand_colors": "Personalizza la tua pagina di prenotazione con i colori del tuo marchio.",
"pro": "Pro",
"removes_cal_branding": "Rimuove tutte le scritte con il marchio {{appName}}, ad es., 'Powered by {{appName}}.'",
"instant_meeting_with_title": "Riunione immediata con {{name}}",
"profile_picture": "Immagine del profilo",
"upload": "Carica",
"add_profile_photo": "Aggiungi foto del profilo",
@@ -1351,6 +1401,7 @@
"event_name_info": "Nome del tipo di evento",
"event_date_info": "Data dell'evento",
"event_time_info": "Ora di inizio dell'evento",
"event_type_not_found": "EventType non trovato",
"location_variable": "Luogo",
"location_info": "Luogo dell'evento",
"additional_notes_variable": "Note aggiuntive",
@@ -1393,6 +1444,7 @@
"slot_length": "Durata della fascia oraria",
"booking_appearance": "Aspetto prenotazione",
"appearance_team_description": "Gestisci le impostazioni di aspetto delle prenotazioni del tuo team",
"appearance_org_description": "Imposta l'aspetto delle prenotazioni per la tua organizzazione",
"only_owner_change": "Solo il proprietario del team può apportare modifiche alla prenotazione del team ",
"team_disable_cal_branding_description": "Rimuove tutte le scritte con il marchio {{appName}}, ad es., 'Powered by {{appName}}'",
"invited_by_team": "{{teamName}} ti ha invitato a unirti al team come {{role}}",
@@ -1446,6 +1498,7 @@
"password_updated": "Password aggiornata!",
"pending_payment": "Pagamento in sospeso",
"pending_invites": "Inviti in sospeso",
"pending_organization_invites": "Inviti in sospeso dell'organizzazione",
"not_on_cal": "Non presente su {{appName}}",
"no_calendar_installed": "Nessun calendario installato",
"no_calendar_installed_description": "Non hai ancora collegato alcun calendario",
@@ -1457,6 +1510,8 @@
"report_app": "Segnala app",
"limit_booking_frequency": "Limita frequenza di prenotazione",
"limit_booking_frequency_description": "Indica quante volte è possibile prenotare questo evento",
"limit_booking_only_first_slot": "Limita prenotazione solo prima fascia",
"limit_booking_only_first_slot_description": "Consenti la prenotazione solo nella prima fascia oraria di ogni giorno",
"limit_total_booking_duration": "Limita durata totale prenotazione",
"limit_total_booking_duration_description": "Indicare la quantità di tempo totale disponibile per prenotare questo evento",
"add_limit": "Aggiungi limite",
@@ -1524,10 +1579,14 @@
"your_org_disbanded_successfully": "La tua organizzazione è stata sciolta",
"error_creating_team": "Errore durante la creazione del team",
"you": "Tu",
"or_continue_with": "Oppure continua con",
"resend_email": "Invia di nuovo l'e-mail",
"member_already_invited": "Membro già invitato",
"already_in_use_error": "Nome utente già in uso",
"enter_email_or_username": "Immettere un indirizzo e-mail o un nome utente",
"enter_email": "Inserisci un'email",
"enter_emails": "Inserisci email",
"too_many_invites": "Puoi invitare un massimo di {{nbUsers}} utenti alla volta.",
"team_name_taken": "Nome già utilizzato",
"must_enter_team_name": "Necessario immettere un nome per il team",
"team_url_required": "Immettere un URL per il team",
@@ -1607,6 +1666,7 @@
"individual": "Persona",
"all_bookings_filter_label": "Tutte le prenotazioni",
"all_users_filter_label": "Tutti gli utenti",
"all_event_types_filter_label": "Tutti i tipi di evento",
"your_bookings_filter_label": "Le tue prenotazioni",
"meeting_url_variable": "URL della riunione",
"meeting_url_info": "URL dell'evento, dell'incontro o della conferenza",
@@ -1703,9 +1763,11 @@
"organizer_timezone": "Fuso orario organizzatore",
"email_user_cta": "Visualizza invito",
"email_no_user_invite_heading_team": "Hai ricevuto un invito a entrare a far parte del team di {{appName}}",
"email_no_user_invite_heading_subteam": "Sei stato invitato a partecipare a un team dell'organizzazione {{parentTeamName}}",
"email_no_user_invite_heading_org": "Hai ricevuto un invito a entrare a far parte dell'organizzazione {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} ti ha invitato a unirti al suo team su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette al tuo team di pianificare le riunioni senza scambiare decine di e-mail.",
"email_user_invite_subheading_team": "{{invitedBy}} ti ha invitato a unirti al suo team `{{teamName}}` su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette a te e al tuo team di pianificare le riunioni senza scambiare decine di e-mail.",
"email_user_invite_subheading_subteam": "{{invitedBy}} ti ha invitato a entrare nel team {{teamName}} della sua organizzazione {{parentTeamName}} su {{appName}}. {{appName}} è lo strumento di programmazione degli eventi che consente a te e al tuo team di programmare riunioni senza lo scambio di infinite email.",
"email_user_invite_subheading_org": "{{invitedBy}} ti ha invitato a unirti alla sua organizzazione `{{teamName}}` su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette a te e alla tua organizzazione di pianificare le riunioni senza scambiare decine di e-mail.",
"email_no_user_invite_steps_intro": "Ti assisteremo nei passaggi iniziali e potrai rapidamente pianificare eventi per il/la {{entity}} senza fatica.",
"email_no_user_step_one": "Scegli il tuo nome utente",
@@ -1838,7 +1900,9 @@
"review_event_type": "Rivedi tipo di evento",
"looking_for_more_analytics": "Hai bisogno di più dati analitici?",
"looking_for_more_insights": "Ti servono più Insights?",
"filters": "Filtri",
"add_filter": "Aggiungi filtro",
"remove_filters": "Cancella tutti i filtri",
"select_user": "Seleziona utente",
"select_event_type": "Seleziona tipo di evento",
"select_date_range": "Seleziona intervallo di date",
@@ -1929,6 +1993,7 @@
"connect_google_workspace": "Collega Google Workspace",
"google_workspace_admin_tooltip": "Per usare questa funzione è necessario essere amministratori di Workspace",
"first_event_type_webhook_description": "Crea il primo webhook per questo tipo di evento",
"create_instant_meeting_webhook_description": "Crea il tuo primo webhook con \"Riunione immediata creata\" come attivatore per questo tipo di evento",
"install_app_on": "Installa l'app su",
"create_for": "Crea per",
"currency": "Valuta",
@@ -1993,6 +2058,8 @@
"add_times_to_your_email": "Seleziona alcuni orari disponibili e incorporali nella tua e-mail",
"select_time": "Seleziona l'ora",
"select_date": "Seleziona la data",
"connecting_you_to_someone": "Ti stiamo collegando con qualcuno.",
"please_do_not_close_this_tab": "Non chiudere questa scheda",
"see_all_available_times": "Vedi tutti gli orari disponibili",
"org_team_names_example_1": "ad es., team di marketing",
"org_team_names_example_2": "ad es., team vendite",
@@ -2010,8 +2077,12 @@
"description_requires_booker_email_verification": "Per garantire la verifica dell'indirizzo e-mail della persona che prenota prima di panificare eventi",
"requires_confirmation_mandatory": "I messaggi di testo possono solo essere inviati ai partecipanti quando il tipo di evento richiede una conferma.",
"organizations": "Organizzazioni",
"upload_cal_video_logo": "Carica il logo Cal Video",
"update_cal_video_logo": "Aggiorna il logo Cal Video",
"cal_video_logo_upload_instruction": "Per fare in modo che il tuo logo sia visibile sullo sfondo scuro del video Cal, carica un'immagine di colore chiaro in formato PNG o SVG per mantenere la trasparenza.",
"org_admin_other_teams": "Atri team",
"org_admin_other_teams_description": "Qui puoi vedere i team di cui non fai parte all'interno della tua organizzazione. Puoi entrare a far parte di tali team se necessario.",
"not_part_of_org": "Non fai parte di alcuna organizzazione",
"no_other_teams_found": "Nessun altro team trovato",
"no_other_teams_found_description": "Non ci sono altri team in questa organizzazione.",
"attendee_first_name_variable": "Nome del partecipante",
@@ -2047,7 +2118,9 @@
"org_error_processing": "Si è verificato un errore durante l'elaborazione di questa organizzazione",
"orgs_page_description": "Un elenco di tutte le organizzazioni. Accettando un'organizzazione, tutti gli utenti con il rispettivo dominio e-mail potranno registrarsi SENZA verificare l'indirizzo e-mail.",
"unverified": "Non verificato",
"verified": "Verificato",
"dns_missing": "DNS mancante",
"dns_configured": "DNS Configurato",
"mark_dns_configured": "Contrassegna DNS come configurato",
"value": "Valore",
"your_organization_updated_sucessfully": "L'organizzazione è stata aggiornata correttamente",
@@ -2057,8 +2130,11 @@
"oAuth": "OAuth",
"recently_added": "Aggiunti di recente",
"connect_all_calendars": "Collega tutti i tuoi calendari",
"connect_all_calendars_description": "{{appName}} legge la disponibilità da tutti i tuoi calendari esistenti.",
"workflow_automation": "Automazione dei flussi di lavoro",
"workflow_automation_description": "Personalizza la tua esperienza di programmazione con i flussi di lavoro",
"scheduling_for_your_team": "Automazione dei flussi di lavoro",
"scheduling_for_your_team_description": "Usa la programmazione collettiva e round robin per il tuo team",
"no_members_found": "Nessun membro trovato",
"event_setup_length_error": "Impostazione evento: la durata deve essere di almeno 1 minuto.",
"availability_schedules": "Calendario disponibilità",
@@ -2091,10 +2167,100 @@
"overlay_my_calendar": "Sovrapponi il mio calendario",
"overlay_my_calendar_toc": "Collegando il tuo calendario, accetti la nostra informativa sulla privacy e i termini di servizio. Puoi revocare l'accesso in qualsiasi momento.",
"view_overlay_calendar_events": "Visualizza gli eventi del tuo calendario per prevenire prenotazioni in conflitto.",
"join_event_location": "Partecipa a {{eventLocationType}}",
"troubleshooting": "Risoluzione problemi",
"calendars_were_checking_for_conflicts": "Calendari in cui stiamo cercando possibili conflitti",
"availabilty_schedules": "Programmi di disponibilità",
"manage_calendars": "Gestisci calendari",
"manage_availability_schedules": "Gestisci programmi di disponibilità",
"lock_timezone_toggle_on_booking_page": "Blocca fuso orario nella pagina di prenotazione",
"description_lock_timezone_toggle_on_booking_page": "Per bloccare il fuso orario nella pagina di prenotazione, utile per gli eventi di persona.",
"event_setup_multiple_payment_apps_error": "Puoi avere solo un'app di pagamento abilitata per ciascun tipo di evento.",
"number_in_international_format": "Inserisci il numero in formato internazionale.",
"install_calendar": "Installa Calendario",
"branded_subdomain": "Sottodominio del marchio",
"branded_subdomain_description": "Crea un sottodominio per il tuo marchio, come acme.cal.com",
"org_insights": "Approfondimenti a livello di organizzazione",
"org_insights_description": "Comprendi come la tua organizzazione utilizza il tempo",
"extensive_whitelabeling": "Assistenza per l'onboarding e supporto tecnico dedicati",
"extensive_whitelabeling_description": "Personalizza la tua esperienza di programmazione con il tuo logo, colori e altro ancora",
"unlimited_teams": "Numero illimitato di team",
"unlimited_teams_description": "Aggiungi alla tua organizzazione tutti i sotto-team necessari",
"unified_billing": "Fatturazione unificata",
"unified_billing_description": "Indica una singola carta di credito per pagare tutti gli abbonamenti del tuo team",
"advanced_managed_events": "Tipi di evento gestiti avanzati",
"advanced_managed_events_description": "Indica una singola carta di credito per pagare tutti gli abbonamenti del tuo team",
"enterprise_description": "Passa a Enterprise per creare la tua organizzazione",
"create_your_org": "Crea la tua organizzazione",
"create_your_org_description": "Passa a Enterprise per avere sottodominio, fatturazione unificata, approfondimenti, personalizzazione completa e altro ancora",
"other_payment_app_enabled": "Puoi attivare solo un'app di pagamento per ciascun tipo di evento",
"admin_delete_organization_description": "<ul><li>Verranno eliminati anche i team che sono membri di questa organizzazione, insieme ai rispettivi tipi di evento</li><li>Gli utenti che facevano parte dell'organizzazione non verranno eliminati e anche i loro tipi di evento rimarranno inalterati.</li><li>I nomi utente saranno modificati in modo che possano esistere al di fuori dell'organizzazione</li></ul>",
"admin_delete_organization_title": "Eliminare {{organizationName}}?",
"published": "Pubblicato",
"unpublished": "Pubblicazione annullata",
"publish": "Pubblica",
"org_publish_error": "Non è stato possibile pubblicare l'organizzazione",
"troubleshooter_tooltip": "Apri lo strumento di soluzione dei problemi per individuare il problema del tuo programma",
"need_help": "Hai bisogno di aiuto?",
"troubleshooter": "Risoluzione problemi",
"please_install_a_calendar": "Installa un calendario",
"instant_tab_title": "Prenotazione immediata",
"instant_event_tab_description": "Consenti alle persone di prenotare immediatamente",
"uprade_to_create_instant_bookings": "Passa a Enterprise e permetti agli ospiti di partecipare a una chiamata immediata a cui i partecipanti possono accedere direttamente. Ciò è riservato agli eventi per team",
"dont_want_to_wait": "Non vuoi aspettare?",
"meeting_started": "Riunione iniziata",
"pay_and_book": "Pagare per prenotare",
"booking_not_found_error": "Prenotazione non trovata",
"booking_seats_full_error": "I posti sono esauriti",
"missing_payment_credential_error": "Credenziali di pagamento mancanti",
"missing_payment_app_id_error": "ID dell'app di pagamento mancante",
"not_enough_available_seats_error": "La prenotazione non ha abbastanza posti disponibili",
"user_redirect_title": "{{username}} è attualmente assente per un breve periodo.",
"user_redirect_description": "Nel frattempo, {{profile.username}} sarà responsabile di tutte le nuove riunioni programmate per conto di {{username}}.",
"out_of_office": "Assente",
"out_of_office_description": "Configura le azioni nel tuo profilo mentre sei assente.",
"send_request": "Invia richiesta",
"start_date_and_end_date_required": "Data iniziale e data finale sono obbligatorie",
"start_date_must_be_before_end_date": "La data iniziale deve precedere quella finale",
"start_date_must_be_in_the_future": "La data iniziale deve essere nel futuro",
"user_not_found": "Utente non trovato",
"out_of_office_entry_already_exists": "La voce per l'assenza esiste già",
"out_of_office_id_required": "L'ID della voce per l'assenza è obbligatorio",
"booking_redirect_infinite_not_allowed": "Esiste già un reindirizzamento della prenotazione dall'utente a te.",
"success_entry_created": "Nuova voce creata correttamente",
"booking_redirect_email_subject": "Notifica di reindirizzamento prenotazione",
"booking_redirect_email_title": "Notifica di reindirizzamento prenotazione",
"booking_redirect_email_description": "Hai ricevuto un reindirizzamento delle prenotazioni da {{toName}}. I link del suo profilo rimanderanno ai tuoi, nel periodo: ",
"success_accept_booking_redirect": "Hai accettato questa richiesta di reindirizzamento delle prenotazioni.",
"success_reject_booking_redirect": "Hai rifiutato questa richiesta di reindirizzamento delle prenotazioni.",
"copy_link_booking_redirect_request": "Copia link per condividere la richiesta",
"booking_redirect_request_title": "Richiesta di reindirizzamento delle prenotazioni",
"select_team_member": "Seleziona un membro del team",
"going_away_title": "Devi assentarti? Segnala che il link al tuo profilo non è disponibile per un periodo.",
"redirect_team_enabled": "Rendirizza il tuo profilo a un altro membro del team",
"redirect_team_disabled": "Reindirizza il tuo profilo a un altro membro del team (è necessario il piano Team)",
"out_of_office_unavailable_list": "Elenco di indisponibilità per assenza",
"success_deleted_entry_out_of_office": "Voce eliminata",
"temporarily_out_of_office": "Temporaneamente assente?",
"add_a_redirect": "Aggiungi un reindirizzamento",
"create_entry": "Crea una voce",
"time_range": "Intervallo temporale",
"automatically_add_all_team_members": "Aggiungi tutti i membri del team, compresi quelli futuri",
"redirect_to": "Reindirizza a",
"having_trouble_finding_time": "Non riesci a trovare un orario?",
"show_more": "Mostra altro",
"assignment_description": "Pianifica le riunioni quando tutti sono disponibili, oppure ruota tra i membri del tuo team",
"lowest": "più basso",
"low": "basso",
"medium": "medio",
"high": "alto",
"Highest": "più alto",
"send_booker_to": "Rimanda la persona che prenota a",
"set_priority": "Imposta priorità",
"priority_for_user": "Priorità per {{userName}}",
"change_priority": "cambia priorità",
"field_identifiers_as_variables": "Usa gli identificatori di campo come variabili per il reindirizzamento dell'evento personalizzato",
"field_identifiers_as_variables_with_example": "Usa gli identificatori di campo come variabili per il reindirizzamento dell'evento personalizzato (es: {{variable}})",
"account_already_linked": "L'account è già collegato",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -13,10 +13,17 @@
"reset_password_subject": "{{appName}}: Instruções para redefinir sua senha",
"verify_email_subject": "{{appName}}: Verificar sua conta",
"check_your_email": "Verifique seu e-mail",
"old_email_address": "E-mail antigo",
"new_email_address": "Novo e-mail",
"verify_email_page_body": "Enviamos um e-mail para {{email}}. É importante verificar seu endereço de e-mail para garantir o melhor envio de e-mail e calendário do {{appName}}.",
"verify_email_banner_body": "Verifique seu endereço de e-mail para garantir o fornecimento do melhor e-mail e calendário",
"verify_email_email_header": "Verifique seu endereço de e-mail",
"verify_email_email_button": "Verificar e-mail",
"verify_email_change_description": "Você solicitou recentemente a alteração do endereço de e-mail que você usa para entrar em sua conta {{appName}}. Por favor, clique no botão abaixo para confirmar o seu novo endereço de e-mail.",
"verify_email_change_success_toast": "Seu e-mail foi atualizado para {{email}}",
"verify_email_change_failure_toast": "Não foi possível atualizar o e-mail.",
"change_of_email": "Verifique seu novo endereço de e-mail da sua conta {{appName}}",
"change_of_email_toast": "Enviamos um link de verificação para {{email}}. Atualizaremos seu endereço de e-mail quando você clicar neste link.",
"copy_somewhere_safe": "Salve esta chave de API em um lugar seguro. Você não será capaz de vê-la novamente.",
"verify_email_email_body": "Por favor, verifique seu endereço de e-mail clicando no botão abaixo.",
"verify_email_by_code_email_body": "Por favor, verifique seu endereço de e-mail usando o código abaixo.",
@@ -56,6 +63,17 @@
"a_refund_failed": "Um reembolso falhou",
"awaiting_payment_subject": "Aguardando pagamento: {{title}} em {{date}}",
"meeting_awaiting_payment": "Sua reunião está aguardando o pagamento",
"dark_theme_contrast_error": "A cor do tema escuro não foi aprovada pela verificação de contraste. Recomendamos que você altere esta cor para que seus botões fiquem mais visíveis.",
"light_theme_contrast_error": "A cor do tema claro não foi aprovada pela verificação de contraste. Recomendamos que você altere esta cor para que seus botões fiquem mais visíveis.",
"payment_not_created_error": "O pagamento não pôde ser criado",
"couldnt_charge_card_error": "Não foi possível cobrar o cartão para o pagamento",
"no_available_users_found_error": "Nenhum usuário disponível foi encontrado. Você poderia tentar outra faixa horária?",
"request_body_end_time_internal_error": "Erro interno. O corpo da solicitação não contém a hora final",
"create_calendar_event_error": "Não foi possível criar um evento no calendário do organizador",
"update_calendar_event_error": "Não foi possível atualizar o evento.",
"delete_calendar_event_error": "Não foi possível deletar o evento.",
"already_signed_up_for_this_booking_error": "Você já está inscrito para esta reserva.",
"hosts_unavailable_for_booking": "Alguns dos anfitriões não estão disponíveis para reserva.",
"help": "Ajuda",
"price": "Preço",
"paid": "Pago",
@@ -101,6 +119,7 @@
"requested_to_reschedule_subject_attendee": "Ação de reagendamento necessária: reserve outro horário para {{eventType}} com {{name}}",
"hi_user_name": "Olá {{name}}",
"ics_event_title": "{{eventType}} com {{name}}",
"please_book_a_time_sometime_later": "Ops, não foi possível conectá-lo desta vez. Por favor, agende uma chamada mais tarde",
"new_event_subject": "Novo evento: {{attendeeName}} - {{date}} - {{eventType}}",
"join_by_entrypoint": "Junte-se através de {{entryPoint}}",
"notes": "Notas",
@@ -117,15 +136,20 @@
"meeting_id": "ID da reunião",
"meeting_password": "Senha da reunião",
"meeting_url": "URL da reunião",
"meeting_url_not_found": "URL da reunião não encontrada",
"token_not_found": "Token não encontrado",
"some_other_host_already_accepted_the_meeting": "Algum outro anfitrião já aceitou a reunião. Ainda deseja entrar? <1>Entrar na reunião</1>",
"meeting_request_rejected": "A sua solicitação de reunião foi rejeitada",
"rejected_event_type_with_organizer": "Rejeitado: {{eventType}} com {{organizer}} em {{date}}",
"hi": "Olá",
"join_team": "Entrar no time",
"manage_this_team": "Gerenciar esta equipe",
"team_info": "Informações da equipe",
"join_meeting": "Entrar na reunião",
"request_another_invitation_email": "Se não quiser utilizar {{toEmail}} como email do {{appName}}, ou já tem uma conta em {{appName}}, por favor peça outro convite para esse email.",
"you_have_been_invited": "Você recebeu um convite para se juntar ao time {{teamName}}",
"user_invited_you": "{{user}} convidou para ingressar na equipe {{team}} de {{entity}} em {{appName}}",
"user_invited_you_to_subteam": "{{user}} convidou você para se juntar à equipe {{team}} da organização {{parentTeamName}} no {{appName}}",
"hidden_team_member_title": "Você está oculto nesta equipe",
"hidden_team_member_message": "Seu assento não é pago, atualize para a versão Pro ou avise o proprietário da equipe de que é possível pagar pelo seu assento.",
"hidden_team_owner_message": "É preciso ter uma conta Pro para usar equipes, você permanecerá oculto até a atualização.",
@@ -229,6 +253,7 @@
"reset_your_password": "Defina sua nova senha com as instruções enviadas para seu endereço de e-mail.",
"email_change": "Volte a entrar com seu novo endereço de e-mail e senha.",
"create_your_account": "Criar sua conta",
"create_your_calcom_account": "Crie sua conta Cal.com",
"sign_up": "Cadastro",
"youve_been_logged_out": "A sessão foi encerrada",
"hope_to_see_you_soon": "Esperamos te ver novamente em breve!",
@@ -266,6 +291,9 @@
"nearly_there_instructions": "Por último, uma breve descrição de você mesmo e uma foto ajudam a conseguir agendamentos e a mostrar às pessoas com quem elas estão agendando.",
"set_availability_instructions": "Definir períodos em que você está disponível de forma recorrente. Mais tarde você poderá criar outros períodos e atribuí-los a diferentes calendários.",
"set_availability": "Definir disponibilidade",
"set_availbility_description": "Defina os horários que você quer que estejam disponíveis para reservas.",
"share_a_link_or_embed": "Compartilhe um link ou incorpore",
"share_a_link_or_embed_description": "Compartilhe o link da sua conta {{appName}} ou incorpore no seu site.",
"availability_settings": "Configurações de disponibilidade",
"continue_without_calendar": "Continuar sem calendário",
"continue_with": "Continuar com {{appName}}",
@@ -281,6 +309,7 @@
"welcome_to_calcom": "Bem vindo ao {{appName}}",
"welcome_instructions": "Diga como devemos te chamar e qual o seu fuso horário. Você poderá editar isto mais tarde.",
"connect_caldav": "Conectar a um servidor CalDav",
"connect_ics_feed": "Conectar a um feed ICS",
"connect": "Conectar",
"try_for_free": "Experimente gratuitamente",
"create_booking_link_with_calcom": "Crie o seu próprio link para reservas com {{appName}}",
@@ -419,6 +448,7 @@
"browse_api_documentation": "Consulte a nossa documentação da API",
"leverage_our_api": "Utilize nossa API para controle e personalização completa.",
"create_webhook": "Criar Webhook",
"instant_meeting": "Reunião instantânea criada",
"booking_cancelled": "Reserva cancelada",
"booking_rescheduled": "Reserva Reagendada",
"recording_ready": "Link para download de gravação pronto",
@@ -533,6 +563,7 @@
"enter_number_between_range": "Por favor, insira um número entre 1 e {{maxOccurences}}",
"email_address": "Endereço de e-mail",
"enter_valid_email": "Insira um e-mail válido",
"please_schedule_future_call": " Por favor, agende uma chamada futura se não estivermos disponíveis em {{seconds}} segundos",
"location": "Local",
"address": "Local",
"enter_address": "Insira o endereço",
@@ -592,6 +623,7 @@
"new_member": "Novo Membro",
"invite": "Convidar",
"add_team_members": "Adicionar membros da equipe",
"add_org_members": "Adicionar membros",
"add_team_members_description": "Convidar outros para se juntar à sua equipe",
"add_team_member": "Adicionar membro da equipe",
"invite_new_member": "Convidar um novo membro",
@@ -606,6 +638,7 @@
"hide_book_a_team_member_description": "Oculte o botão de agendar um membro da equipe em suas páginas públicas.",
"danger_zone": "Zona de perigo",
"account_deletion_cannot_be_undone": "Cuidado. A exclusão da conta não pode ser desfeita.",
"team_deletion_cannot_be_undone": "Cuidado. A exclusão de uma equipe não pode ser desfeita",
"back": "Voltar",
"cancel": "Cancelar",
"cancel_all_remaining": "Cancelar todos os restantes",
@@ -668,6 +701,7 @@
"add_members": "Adicionar membros...",
"no_assigned_members": "Nenhum membro atribuído",
"assigned_to": "Atribuído a",
"you_must_be_logged_in_to": "Você deve estar logado em {{url}}",
"start_assigning_members_above": "Começar a atribuir os membros acima",
"locked_fields_admin_description": "Os membros não poderão editar isto",
"locked_fields_member_description": "Esta opção foi bloqueada pelo administrador da equipe",
@@ -772,6 +806,7 @@
"additional_input_description": "Solicitar que o agendador insira entradas adicionais antes que o pedido seja confirmado",
"label": "Etiqueta",
"placeholder": "Preenchimento",
"display_add_to_calendar_organizer": "Exibir o e-mail 'Adicionar ao calendário' como organizador",
"type": "Tipo",
"edit": "Editar",
"add_input": "Adicionar um campo",
@@ -13,10 +13,17 @@
"reset_password_subject": "{{appName}}: Instruções de redefinição da senha",
"verify_email_subject": "{{appName}}: Verifique a sua conta",
"check_your_email": "Verifique o seu e-mail",
"old_email_address": "E-mail antigo",
"new_email_address": "Novo e-mail",
"verify_email_page_body": "Enviámos um e-mail para {{email}}. É importante verificar o seu endereço de e-mail para garantir que receberá as comunicações de {{appName}}.",
"verify_email_banner_body": "Confirme o seu endereço de e-mail para garantir a melhor entrega possível de e-mail e de agenda",
"verify_email_email_header": "Confirme o seu endereço de e-mail",
"verify_email_email_button": "Confirmar o seu e-mail",
"verify_email_change_description": "Solicitou recentemente a alteração do endereço de e-mail que utiliza para entrar na sua conta {{appName}}. Clique no botão abaixo para confirmar o seu novo endereço de e-mail.",
"verify_email_change_success_toast": "Atualizou o seu e-mail para {{email}}",
"verify_email_change_failure_toast": "Falha ao atualizar o e-mail.",
"change_of_email": "Confirme o seu novo endereço de e-mail para {{appName}}",
"change_of_email_toast": "Enviámos uma ligação de confirmação para {{email}}. Atualizaremos o seu endereço de e-mail após clicar nessa ligação.",
"copy_somewhere_safe": "Guarde esta chave da API num local seguro. Não poderá consultar a mesma novamente.",
"verify_email_email_body": "Confirme o seu endereço de e-mail clicando no botão abaixo.",
"verify_email_by_code_email_body": "Confirme o seu endereço de e-mail utilizando o código abaixo.",
@@ -142,6 +149,7 @@
"request_another_invitation_email": "Se não quiser utilizar {{toEmail}} como email do {{appName}}, ou já tem uma conta em {{appName}}, por favor peça outro convite para esse email.",
"you_have_been_invited": "Recebeu um convite para juntar-se à equipa {{teamName}}",
"user_invited_you": "{{user}} enviou um convite para se juntar a {{team}} de {{entity}} em {{appName}}",
"user_invited_you_to_subteam": "{{user}} enviou um convite para se juntar à equipa {{team}} da organização {{parentTeamName}} em {{appName}}",
"hidden_team_member_title": "Está oculto nesta equipa",
"hidden_team_member_message": "O seu lugar não está pago, faça upgrade para PRO ou informe ao proprietário da equipa que pode pagar pelo seu lugar.",
"hidden_team_owner_message": "Precisa de uma conta Pro para usar equipas, ficará oculto até que actualize.",
@@ -245,6 +253,7 @@
"reset_your_password": "Defina a sua nova palavra-passe seguindo as instruções enviadas para o seu endereço de e-mail.",
"email_change": "Inicie sessão novamente com o seu novo endereço de e-mail e palavra-passe.",
"create_your_account": "Crie a sua conta",
"create_your_calcom_account": "Crie a sua conta Cal.com",
"sign_up": "Registar",
"youve_been_logged_out": "A sessão foi encerrada",
"hope_to_see_you_soon": "Esperamos ver-nos em breve de novo!",
@@ -282,6 +291,9 @@
"nearly_there_instructions": "Por último, uma breve descrição sobre si e uma fotografia ajudam a obter reservas e a mostrar às pessoas com quem estão a reservar.",
"set_availability_instructions": "Definir períodos em que está disponível numa base recorrente. Mais tarde poderá criar mais destes, e atribuí-los a diferentes calendários.",
"set_availability": "Definir disponibilidade",
"set_availbility_description": "Defina agendamentos para os horários que pretende que sejam reservados.",
"share_a_link_or_embed": "Partilhe uma ligação ou incorpore",
"share_a_link_or_embed_description": "Partilhe a sua ligação {{appName}} ou incorpore no seu site.",
"availability_settings": "Configurações de disponibilidade",
"continue_without_calendar": "Continuar sem calendário",
"continue_with": "Continuar com {{appName}}",
@@ -297,6 +309,7 @@
"welcome_to_calcom": "Boas-vindas ao {{appName}}",
"welcome_instructions": "Diga-nos como lhe devemos chamar e qual o seu fuso horário. Poderá editar isto mais tarde.",
"connect_caldav": "Ligar a servidor CalDav",
"connect_ics_feed": "Ligue-se a uma Feed ICS",
"connect": "Ligar",
"try_for_free": "Experimente gratuitamente",
"create_booking_link_with_calcom": "Crie a sua própria ligação de reservas com {{appName}}",
@@ -435,6 +448,7 @@
"browse_api_documentation": "Consulte a nossa documentação da API",
"leverage_our_api": "Tire partido da nossa API para controlo e personalização total.",
"create_webhook": "Criar Webhook",
"instant_meeting": "Reunião instantânea criada",
"booking_cancelled": "Reserva cancelada",
"booking_rescheduled": "Reserva Reagendada",
"recording_ready": "Ligação de transferência da gravação pronta",
@@ -549,6 +563,7 @@
"enter_number_between_range": "Introduza um número entre 1 e {{maxOccurences}}",
"email_address": "Endereço de email",
"enter_valid_email": "Insira um e-mail válido",
"please_schedule_future_call": " Se não estivermos disponíveis em {{seconds}} segundos, por favor, agende uma chamada para mais tarde",
"location": "Localização",
"address": "Local",
"enter_address": "Especifique o endereço",
@@ -608,6 +623,7 @@
"new_member": "Novo Membro",
"invite": "Convidar",
"add_team_members": "Adicionar membros da equipa",
"add_org_members": "Adicionar membros",
"add_team_members_description": "Convidar outros a juntarem-se à sua equipa",
"add_team_member": "Adicionar membro da equipa",
"invite_new_member": "Convidar um Novo Membro",
@@ -622,6 +638,7 @@
"hide_book_a_team_member_description": "Ocultar o botão Reservar um Membro da equipa das suas páginas públicas.",
"danger_zone": "Zona de perigo",
"account_deletion_cannot_be_undone": "Cuidado. A eliminação da conta não pode ser anulada.",
"team_deletion_cannot_be_undone": "Cuidado. A eliminação de uma equipa não pode ser anulada",
"back": "Anterior",
"cancel": "Cancelar",
"cancel_all_remaining": "Cancelar todos os restantes",
@@ -672,6 +689,7 @@
"default_duration": "Duração predefinida",
"default_duration_no_options": "Por favor, selecione primeiro as durações disponíveis",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minutos",
"round_robin": "Round Robin",
"round_robin_description": "Reuniões de ciclo entre vários membros da equipa.",
@@ -684,6 +702,7 @@
"add_members": "Adicionar membros...",
"no_assigned_members": "Sem membros atribuídos",
"assigned_to": "Atribuído a",
"you_must_be_logged_in_to": "Deve ter sessão iniciada para {{url}}",
"start_assigning_members_above": "Comece a atribuir membros acima",
"locked_fields_admin_description": "Os membros não poderão editar isto",
"locked_fields_member_description": "Esta opção foi bloqueada pelo administrador da equipa",
@@ -788,6 +807,7 @@
"additional_input_description": "Requerer que o responsável pelo agendamento especifique entradas complementares antes do pedido ser confirmado",
"label": "Etiqueta",
"placeholder": "Preenchimento",
"display_add_to_calendar_organizer": "Mostrar o e-mail 'Adicionar ao calendário' como organizador",
"type": "Tipo",
"edit": "Editar",
"add_input": "Adicionar um Campo",
@@ -796,6 +816,9 @@
"requires_confirmation_description": "A reserva precisa ser confirmada manualmente antes de ser enviada para as integrações e ser enviada uma mensagem de confirmação.",
"recurring_event": "Evento recorrente",
"recurring_event_description": "As pessoas podem subscrever para eventos recorrentes",
"cannot_be_used_with_paid_event_types": "Não pode ser utilizado com tipos de eventos pagos",
"warning_payment_instant_meeting_event": "As Reuniões Instantâneas ainda não são suportadas com eventos recorrentes e aplicações de pagamento",
"warning_instant_meeting_experimental": "Experimental: atualmente os Eventos de Reuniões Instantâneas são experimentais.",
"starting": "A iniciar",
"disable_guests": "Desativar Convidados",
"disable_guests_description": "Desativar a adição de convidados adicionais durante a reserva.",
@@ -804,6 +827,7 @@
"private_link_label": "Ligação privada",
"private_link_hint": "A sua ligação será renovada após cada utilização",
"copy_private_link": "Copiar ligação privada",
"copy_private_link_to_event": "Copiar ligação privada para o evento",
"private_link_description": "Crie um URL privado para partilhar sem expor o seu nome de utilizador do {{appName}}",
"invitees_can_schedule": "Os convidados podem agendar",
"date_range": "Intervalo de Datas",
@@ -863,6 +887,7 @@
"next_step": "Ignorar passo",
"prev_step": "Passo anterior",
"install": "Instalar",
"start_paid_trial": "Iniciar avaliação gratuita",
"installed": "Instalado",
"active_install_one": "{{count}} instalação activa",
"active_install_other": "{{count}} instalações activas",
@@ -877,6 +902,7 @@
"toggle_calendars_conflict": "Escolha os calendários que deseja verificar para evitar conflitos e duplicação de reservas.",
"connect_additional_calendar": "Ligar um calendário adicional",
"calendar_updated_successfully": "Calendário atualizado com sucesso",
"check_here": "Verificar aqui",
"conferencing": "Conferência",
"calendar": "Calendário",
"payments": "Pagamentos",
@@ -1048,6 +1074,9 @@
"user_impersonation_heading": "Representação de utilizador",
"user_impersonation_description": "Permite à nossa equipa de suporte iniciar sessão temporariamente em seu nome para rapidamente resolver quaisquer erros que nos reporte.",
"team_impersonation_description": "Permite que os administradores da sua equipa iniciem a sessão temporariamente em seu nome.",
"cal_signup_description": "Gratuito para pessoas singulares. Planos de equipa para funcionalidades colaborativas.",
"make_org_private": "Tornar a organização privada",
"make_org_private_description": "Os membros da sua organização não poderão ver outros membros da organização quando esta opção estiver ativada.",
"make_team_private": "Tornar a equipa privada",
"make_team_private_description": "Os membros da sua equipa não poderão ver outros membros da equipa quando esta opção estiver ativada.",
"you_cannot_see_team_members": "Não pode ver todos os membros da equipa de uma equipa privada.",
@@ -1107,6 +1136,7 @@
"developer_documentation": "Documentação para programadores",
"get_in_touch": "Entre em contacto",
"contact_support": "Contacte o suporte",
"premium_support": "Suporte Premium",
"community_support": "Apoio da Comunidade",
"feedback": "Feedback",
"submitted_feedback": "Obrigado pelo seu feedback!",
@@ -1270,6 +1300,7 @@
"default_calendar_selected": "Calendário predefinido",
"hide_from_profile": "Ocultar do perfil",
"event_setup_tab_title": "Configuração do evento",
"availability_not_found_in_schedule_error": "Não foi encontrada nenhuma disponibilidade na agenda",
"event_limit_tab_title": "Limites",
"event_limit_tab_description": "Com que frequência pode ser reservado",
"event_advanced_tab_description": "Definições do calendário e muito mais...",
@@ -1290,6 +1321,7 @@
"do_this": "Fazer isto",
"turn_off": "Desligar",
"turn_on": "Ativar",
"cancelled_bookings_cannot_be_rescheduled": "As reservas canceladas não podem ser reagendadas",
"settings_updated_successfully": "Definições atualizadas com sucesso",
"error_updating_settings": "Erro ao atualizar as definições",
"personal_cal_url": "O meu endereço pessoal do {{appName}}",
@@ -1313,6 +1345,7 @@
"customize_your_brand_colors": "Personalize a cor da sua marca na sua página de reservas.",
"pro": "Pro",
"removes_cal_branding": "Remove qualquer identificação de marca relacionada o {{appName}}, por exemplo, 'Com tecnologia {{appName}}.'",
"instant_meeting_with_title": "Reunião instantânea com {{name}}",
"profile_picture": "Imagem do perfil",
"upload": "Carregar",
"add_profile_photo": "Adicionar imagem de perfil",
@@ -1368,6 +1401,7 @@
"event_name_info": "O nome do tipo de evento",
"event_date_info": "A data do evento",
"event_time_info": "A hora de início do evento",
"event_type_not_found": "Tipo de evento não encontrado",
"location_variable": "Localização",
"location_info": "O local do evento",
"additional_notes_variable": "Notas Adicionais",
@@ -1410,6 +1444,7 @@
"slot_length": "Comprimento do espaço",
"booking_appearance": "Aparência da reserva",
"appearance_team_description": "Faça a gestão das definições de aparência da reserva da sua equipa",
"appearance_org_description": "Faça a gestão das definições de aparência da reserva da sua organização",
"only_owner_change": "Só o proprietário desta equipa pode fazer alterações na reserva da equipa ",
"team_disable_cal_branding_description": "Remove qualquer identificação de marca relacionada o {{appName}}, por exemplo, 'Com tecnologia {{appName}}'",
"invited_by_team": "{{teamName}} enviou um convite para se juntar à sua equipa como {{role}}",
@@ -1463,6 +1498,7 @@
"password_updated": "Palavra-passe atualizada!",
"pending_payment": "Pagamento pendente",
"pending_invites": "Convites pendentes",
"pending_organization_invites": "Convites de organização pendentes",
"not_on_cal": "Não existe em {{appName}}",
"no_calendar_installed": "Nenhum calendário instalado",
"no_calendar_installed_description": "Ainda não associou nenhum dos seus calendários",
@@ -1474,6 +1510,8 @@
"report_app": "Reportar aplicação",
"limit_booking_frequency": "Limitar a frequência das reservas",
"limit_booking_frequency_description": "Limitar o número de vezes que este evento pode ser reservado",
"limit_booking_only_first_slot": "Limitar a reserva apenas à primeira vaga",
"limit_booking_only_first_slot_description": "Permitir que apenas a primeira vaga de todos os dias seja reservada",
"limit_total_booking_duration": "Limitar a duração total da reserva",
"limit_total_booking_duration_description": "Limitar o tempo total que pode ser reservado para este evento",
"add_limit": "Adicionar limite",
@@ -1541,10 +1579,14 @@
"your_org_disbanded_successfully": "A sua organização foi dissolvida com sucesso",
"error_creating_team": "Erro ao criar a equipa",
"you": "Você",
"or_continue_with": "Ou continuar com",
"resend_email": "Reenviar e-mail",
"member_already_invited": "O membro já foi convidado",
"already_in_use_error": "O nome de utilizador já está a ser utilizado",
"enter_email_or_username": "Especifique um e-mail ou nome de utilizador",
"enter_email": "Inserir um e-mail",
"enter_emails": "Inserir e-mails",
"too_many_invites": "Está limitado a convidar um máximo de {{nbUsers}} utilizadores de cada vez.",
"team_name_taken": "Este nome já está a ser utilizado",
"must_enter_team_name": "Deve especificar um nome para a equipa",
"team_url_required": "Deve especificar um endereço para a equipa",
@@ -1624,6 +1666,7 @@
"individual": "Individual",
"all_bookings_filter_label": "Todas as reservas",
"all_users_filter_label": "Todos os utilizadores",
"all_event_types_filter_label": "Todos os tipos de evento",
"your_bookings_filter_label": "As suas reservas",
"meeting_url_variable": "URL da reunião",
"meeting_url_info": "URL da reunião de conferência",
@@ -1720,9 +1763,11 @@
"organizer_timezone": "Fuso horário do organizador",
"email_user_cta": "Ver convite",
"email_no_user_invite_heading_team": "Recebeu um convite para fazer parte de uma equipa {{appName}}",
"email_no_user_invite_heading_subteam": "Recebeu um convite para fazer parte de uma equipa da organização {{parentTeamName}}",
"email_no_user_invite_heading_org": "Recebeu um convite para fazer parte de uma organização {{appName}}",
"email_no_user_invite_subheading": "Recebeu um convite de {{invitedBy}} para fazer parte da respetiva equipa em {{appName}}. {{appName}} é um ferramenta de conciliação e agendamento de eventos que lhe permite a si e à sua equipa agendar reuniões sem o pingue-pongue de e-mails.",
"email_user_invite_subheading_team": "Recebeu um convite de {{invitedBy}} para fazer parte da equipa `{{teamName}}` em {{appName}}. {{appName}} é uma ferramenta de conciliação e agendamento de eventos que lhe permite a si e à sua equipa agendar reuniões sem o pingue-pongue de mensagens eletrónicas.",
"email_user_invite_subheading_subteam": "Recebeu um convite de {{invitedBy}} para fazer parte da equipa {{teamName}} da organização {{parentTeamName}} em {{appName}}. {{appName}} é uma ferramenta de conciliação e agendamento de eventos que lhe permite, a si e à sua equipa, agendar reuniões sem o pingue-pongue de mensagens eletrónicas.",
"email_user_invite_subheading_org": "Recebeu um convite de {{invitedBy}} para fazer parte de {{teamName}} da sua organização em {{appName}}. {{appName}} é a solução de agendamento de eventos que lhe permite a si e à sua organização agendar reuniões sem o pingue-pongue de e-mails.",
"email_no_user_invite_steps_intro": "Vamos ajudar com alguns pequenos passos para que você e a sua {{entity}} possam desfrutar rapidamente de uma gestão de agendamentos sem complicações.",
"email_no_user_step_one": "Escolha o seu nome de utilizador",
@@ -1855,7 +1900,9 @@
"review_event_type": "Rever Tipo de Evento",
"looking_for_more_analytics": "À procura de mais análises?",
"looking_for_more_insights": "À procura de mais Insights?",
"filters": "Filtros",
"add_filter": "Adicionar filtro",
"remove_filters": "Limpar todos os filtros",
"select_user": "Selecionar utilizador",
"select_event_type": "Selecionar tipo de evento",
"select_date_range": "Selecionar intervalo de datas",
@@ -1946,6 +1993,7 @@
"connect_google_workspace": "Associar o Google Workspace",
"google_workspace_admin_tooltip": "Tem de ser um Administrador do Workspace para utilizar esta funcionalidade",
"first_event_type_webhook_description": "Crie o seu primeiro webhook para este tipo de evento",
"create_instant_meeting_webhook_description": "Crie o seu primeiro webhook com 'Reunião instantânea criada' como acionador para este tipo de evento",
"install_app_on": "Instalar aplicação em",
"create_for": "Criar para",
"currency": "Moeda",
@@ -2010,6 +2058,8 @@
"add_times_to_your_email": "Selecione alguns horários disponíveis e incorpore-os no seu E-mail",
"select_time": "Selecionar horário",
"select_date": "Selecionar data",
"connecting_you_to_someone": "Estamos a conectá-lo a alguém.",
"please_do_not_close_this_tab": "Não feche este separador",
"see_all_available_times": "Ver todos os horários disponíveis",
"org_team_names_example_1": "Por exemplo, Equipa de Marketing",
"org_team_names_example_2": "Por exemplo, Equipa de Vendas",
@@ -2027,8 +2077,12 @@
"description_requires_booker_email_verification": "Para garantir a confirmação do e-mail do responsável pela reserva antes de agendar eventos",
"requires_confirmation_mandatory": "As mensagens de texto só podem ser enviadas aos participantes quando o tipo de evento requerer confirmação.",
"organizations": "Organizações",
"upload_cal_video_logo": "Carregar logótipo de Cal Video",
"update_cal_video_logo": "Atualizar logótipo de Cal Video",
"cal_video_logo_upload_instruction": "Para garantir que o seu logótipo seja visível contra o fundo escuro do Cal Video, carregue uma imagem de cores claras em formato PNG ou SVG, para manter a transparência.",
"org_admin_other_teams": "Outras equipas",
"org_admin_other_teams_description": "Aqui pode ver equipas dentro da sua organização e das quais não faz parte. Pode adicionar-se às mesmas, se necessário.",
"not_part_of_org": "Você não faz parte de nenhuma organização",
"no_other_teams_found": "Não foram encontradas outras equipas",
"no_other_teams_found_description": "Não há outras equipas nesta organização.",
"attendee_first_name_variable": "Primeiro nome do participante",
@@ -2064,7 +2118,9 @@
"org_error_processing": "Ocorreu um erro ao processar esta organização",
"orgs_page_description": "Uma lista de todas as organizações. Aceitar uma organização permitirá que todos os utilizadores com esse domínio de e-mail se registem SEM a verificação de e-mail.",
"unverified": "Não verificada",
"verified": "Confirmado",
"dns_missing": "DNS em falta",
"dns_configured": "DNS configurado",
"mark_dns_configured": "Marcar como o DNS configurado",
"value": "Valor",
"your_organization_updated_sucessfully": "A sua organização foi atualizada com sucesso",
@@ -2074,8 +2130,11 @@
"oAuth": "OAuth",
"recently_added": "Adicionados recentemente",
"connect_all_calendars": "Associe todos os seus calendários",
"connect_all_calendars_description": "{{appName}} lê a disponibilidade de todos os seus calendários existentes.",
"workflow_automation": "Automação de fluxo de trabalho",
"workflow_automation_description": "Personalize a sua experiência de agendamento com fluxos de trabalho",
"scheduling_for_your_team": "Automação de fluxo de trabalho",
"scheduling_for_your_team_description": "Faça o agendamento para a sua equipa, com agendamento coletivo e distribuição equilibrada",
"no_members_found": "Nenhum membro encontrado",
"event_setup_length_error": "Configuração do Evento: a duração deve ser de, pelo menos, 1 minuto.",
"availability_schedules": "Horários de Disponibilidade",
@@ -2108,10 +2167,100 @@
"overlay_my_calendar": "Sobrepor o meu calendário",
"overlay_my_calendar_toc": "Ao ligar-se ao seu calendário, você aceita a nossa política de privacidade e termos de utilização. Pode revogar o acesso a qualquer momento.",
"view_overlay_calendar_events": "Consulte os eventos do seu calendário para evitar agendamentos conflituantes.",
"join_event_location": "Junte-se a {{eventLocationType}}",
"troubleshooting": "Resolução de problemas",
"calendars_were_checking_for_conflicts": "Calendários que estamos a verificar por conflitos",
"availabilty_schedules": "Horários de disponibilidade",
"manage_calendars": "Gerir calendários",
"manage_availability_schedules": "Gerir horários de disponibilidade",
"lock_timezone_toggle_on_booking_page": "Bloquear fuso horário na página de reserva",
"description_lock_timezone_toggle_on_booking_page": "Para bloquear o fuso horário na página de reservas. Útil para eventos presenciais.",
"event_setup_multiple_payment_apps_error": "Só pode ter uma aplicação de pagamentos ativada por tipo de evento.",
"number_in_international_format": "Insira o número no formato internacional.",
"install_calendar": "Instalar Calendário",
"branded_subdomain": "Subdomínio de marca",
"branded_subdomain_description": "Obtenha o seu próprio subdomínio de marca. Por exemplo, acme.cal.com",
"org_insights": "Insights de toda a organização",
"org_insights_description": "Compreenda como toda a sua organização está a utilizar o tempo",
"extensive_whitelabeling": "Apoio dedicado à integração e engenharia",
"extensive_whitelabeling_description": "Personalize a sua experiência de agendamento com o seu próprio logótipo, cores e muito mais",
"unlimited_teams": "Equipas ilimitadas",
"unlimited_teams_description": "Adicione à sua organização tantas subequipas quanto for necessário",
"unified_billing": "Faturação unificada",
"unified_billing_description": "Adicione um único cartão de crédito para pagar todas as subscrições da sua equipa",
"advanced_managed_events": "Tipos de evento geridos avançados",
"advanced_managed_events_description": "Adicione um único cartão de crédito para pagar todas as subscrições da sua equipa",
"enterprise_description": "Atualize para Enterprise para criar a sua Organização",
"create_your_org": "Crie a sua Organização",
"create_your_org_description": "Atualize para Enterprise e receba um subdomínio, faturação unificada, Insights, personalização abrangente da imagem corporativa e muito mais",
"other_payment_app_enabled": "Só pode ativar uma aplicação de pagamento por tipo de evento",
"admin_delete_organization_description": "<ul><li>As equipas que são membros desta organização também serão eliminadas, em conjunto com os respetivos tipos de evento</li><li>Utilizadores que faziam parte da organização não serão eliminados e os seus tipos de evento também permanecerão intactos.</li><li>Os nomes de utilizador serão alterados para permitir que possam existir fora da organização</li></ul>",
"admin_delete_organization_title": "Eliminar {{organizationName}}?",
"published": "Publicada",
"unpublished": "Não publicada",
"publish": "Publicar",
"org_publish_error": "Não foi possível publicar a organização",
"troubleshooter_tooltip": "Abra o solucionador de problemas e descubra o que está errado com o seu agendamento",
"need_help": "Precisa de ajuda?",
"troubleshooter": "Solucionador de problemas",
"please_install_a_calendar": "Por favor, instale um calendário",
"instant_tab_title": "Reserva imediata",
"instant_event_tab_description": "Permita que as pessoas reservem imediatamente",
"uprade_to_create_instant_bookings": "Atualize para Enterprise e permita que os convidados se juntem a uma chamada instantânea em que os participantes podem entrar diretamente. Isto é apenas para tipos de evento de equipa",
"dont_want_to_wait": "Não quer esperar?",
"meeting_started": "Reunião iniciada",
"pay_and_book": "Pagar para reservar",
"booking_not_found_error": "Não foi possível encontrar a reserva",
"booking_seats_full_error": "Os lugares para reserva estão cheios",
"missing_payment_credential_error": "Credenciais de pagamento em falta",
"missing_payment_app_id_error": "ID da aplicação de pagamento em falta",
"not_enough_available_seats_error": "A reserva não tem lugares disponíveis suficientes",
"user_redirect_title": "{{username}} está ausente por um breve período de tempo.",
"user_redirect_description": "Entretanto, {{profile.username}} ficará responsável por todas as novas reuniões agendadas em nome de {{username}}.",
"out_of_office": "Fora do escritório",
"out_of_office_description": "Configure as ações no seu perfil enquanto estiver ausente.",
"send_request": "Enviar pedido",
"start_date_and_end_date_required": "Data de início e data de fim são necessárias",
"start_date_must_be_before_end_date": "Data de início deve ser anterior à data de fim",
"start_date_must_be_in_the_future": "Data de início deve ser no futuro",
"user_not_found": "Utilizador não encontrado",
"out_of_office_entry_already_exists": "Já existe uma entrada de fora do escritório",
"out_of_office_id_required": "É necessária a ID da entrada de fora do escritório",
"booking_redirect_infinite_not_allowed": "Já existe um redirecionamento de reservas desse utilizador para si.",
"success_entry_created": "Criou uma nova entrada com sucesso",
"booking_redirect_email_subject": "Notificação de redirecionamento de reserva",
"booking_redirect_email_title": "Notificação de redirecionamento de reserva",
"booking_redirect_email_description": "Recebeu um redirecionamento de reservas de {{toName}}, pelo que as ligações do respetivo perfil serão redirecionados para o seu perfil durante o intervalo de tempo: ",
"success_accept_booking_redirect": "Aceitou este pedido de redirecionamento de reservas.",
"success_reject_booking_redirect": "Rejeitou este pedido de redirecionamento de reservas.",
"copy_link_booking_redirect_request": "Copiar ligação para partilhar o pedido",
"booking_redirect_request_title": "Pedido de redirecionamento de reservas",
"select_team_member": "Selecionar o membro da equipa",
"going_away_title": "Vai ausentar-se? Simplesmente marque a ligação do seu perfil como indisponível por um determinado período de tempo.",
"redirect_team_enabled": "Redirecionar o seu perfil para outro membro da equipa",
"redirect_team_disabled": "Redirecionar o seu perfil para outro membro da equipa (é necessário um plano Team)",
"out_of_office_unavailable_list": "Lista de indisponibilidades de fora do escritório",
"success_deleted_entry_out_of_office": "Entrada eliminada com sucesso",
"temporarily_out_of_office": "Temporariamente fora do escritório?",
"add_a_redirect": "Adicionar um redirecionamento",
"create_entry": "Criar entrada",
"time_range": "Intervalo de tempo",
"automatically_add_all_team_members": "Adicionar todos os membros da equipa, incluindo membros futuros",
"redirect_to": "Redirecionar para",
"having_trouble_finding_time": "Com dificuldades em encontrar um horário?",
"show_more": "Mostrar mais",
"assignment_description": "Agende reuniões quando todos estiverem disponíveis ou rode entre os membros da sua equipa",
"lowest": "a mais baixa",
"low": "baixa",
"medium": "média",
"high": "alta",
"Highest": "a mais alta",
"send_booker_to": "Enviar responsável pela reserva para",
"set_priority": "Definir prioridade",
"priority_for_user": "Prioridade para {{userName}}",
"change_priority": "alterar prioridade",
"field_identifiers_as_variables": "Utilize identificadores de campo como variáveis para o seu redirecionamento de evento personalizado",
"field_identifiers_as_variables_with_example": "Utilize identificadores de campo como variáveis para o seu redirecionamento de evento personalizado (ex.: {{variable}})",
"account_already_linked": "A conta já está vinculada",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -13,6 +13,8 @@
"reset_password_subject": "{{appName}}: інструкції зі скидання пароля",
"verify_email_subject": "{{appName}}: підтвердьте свій обліковий запис",
"check_your_email": "Перевірте свою електронну пошту",
"old_email_address": "Стара адреса електронної пошти",
"new_email_address": "Нова адреса електронної пошти",
"verify_email_page_body": "Ми надіслали лист на адресу {{email}}. Підтвердьте свою електронну адресу, щоб уникнути проблем із доставкою листів і даних календаря із застосунку {{appName}}.",
"verify_email_banner_body": "Підтвердьте свою електронну адресу, щоб уникнути проблем із доставкою листів і даних календаря",
"verify_email_email_header": "Підтвердьте свою електронну адресу",
@@ -1447,6 +1447,8 @@
"copy_link_to_form": "将链接复制到表格",
"theme": "主题",
"theme_applies_note": "这仅适用于您的公开预约页面",
"app_theme": "仪表板主题",
"app_theme_applies_note": "这仅在您登录仪表板时适用",
"theme_system": "系统默认设置",
"add_a_team": "添加团队",
"add_webhook_description": "当 {{appName}} 上有活动时实时接收会议数据",
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# Only deploy to production
if [ "$VERCEL_ENV" != "production" ]; then exit 0; fi
checkRoute () {
if [ "$1" != '1' ]; then rm -rf $2; fi
}
# These conditionals are used to remove directories from the build that are not needed in production
# This is to reduce the size of the build and prevent OOM errors
checkRoute "$APP_ROUTER_EVENT_TYPES_ENABLED" app/future/event-types
checkRoute "$APP_ROUTER_SETTINGS_ADMIN_ENABLED" app/future/settings/admin
checkRoute "$APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED" app/future/apps/installed
checkRoute "$APP_ROUTER_APPS_SLUG_ENABLED" app/future/apps/\[slug\]
checkRoute "$APP_ROUTER_APPS_SLUG_SETUP_ENABLED" app/future/apps/\[slug\]/setup
checkRoute "$APP_ROUTER_APPS_CATEGORIES_ENABLED" app/future/apps/categories
checkRoute "$APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED" app/future/apps/categories/\[category\]
checkRoute "$APP_ROUTER_WORKFLOWS_ENABLED" app/future/workflows
checkRoute "$APP_ROUTER_SETTINGS_TEAMS_ENABLED" app/future/settings/teams
checkRoute "$APP_ROUTER_GETTING_STARTED_STEP_ENABLED" app/future/getting-started
checkRoute "$APP_ROUTER_APPS_ENABLED" app/future/apps
checkRoute "$APP_ROUTER_BOOKINGS_STATUS_ENABLED" app/future/bookings
checkRoute "$APP_ROUTER_VIDEO_ENABLED" app/future/video
checkRoute "$APP_ROUTER_TEAMS_ENABLED" app/future/teams
# These are routes that don't have and environment variable to enable or disable them
# Will stop removing gradually as we test and confirm that they are working
rm -rf \
app/future/[user]\
app/future/auth\
app/future/booking\
app/future/connect-and-join\
app/future/d\
app/future/enterprise\
app/future/insights\
app/future/maintenance\
app/future/more\
app/future/org\
app/future/payment\
app/future/reschedule\
app/future/routing-forms\
app/future/settings\
app/future/signup\
app/future/team
exit 1
@@ -42,6 +42,7 @@ describe("handleChildrenEventTypes", () => {
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(result.newUserIds).toEqual(undefined);
expect(result.oldUserIds).toEqual(undefined);
@@ -62,6 +63,7 @@ describe("handleChildrenEventTypes", () => {
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(result.newUserIds).toEqual(undefined);
expect(result.oldUserIds).toEqual(undefined);
@@ -88,6 +90,7 @@ describe("handleChildrenEventTypes", () => {
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(result.newUserIds).toEqual(undefined);
expect(result.oldUserIds).toEqual(undefined);
@@ -125,6 +128,7 @@ describe("handleChildrenEventTypes", () => {
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
data: {
@@ -173,13 +177,14 @@ describe("handleChildrenEventTypes", () => {
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {
bookingLimits: undefined,
},
});
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...evType,
hidden: false,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
hashedLink: { create: { link: expect.any(String) } },
},
where: {
@@ -207,6 +212,7 @@ describe("handleChildrenEventTypes", () => {
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(result.newUserIds).toEqual([]);
expect(result.oldUserIds).toEqual([]);
@@ -232,6 +238,7 @@ describe("handleChildrenEventTypes", () => {
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
// Have been called
expect(result.newUserIds).toEqual([5]);
@@ -270,6 +277,7 @@ describe("handleChildrenEventTypes", () => {
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
data: {
@@ -318,14 +326,15 @@ describe("handleChildrenEventTypes", () => {
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {
length: 30,
},
});
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...evType,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
scheduleId: undefined,
hidden: false,
length: 30,
hashedLink: undefined,
},
where: {
userId_parentId: {
@@ -379,6 +388,7 @@ describe("handleChildrenEventTypes", () => {
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
data: {
@@ -404,13 +414,8 @@ describe("handleChildrenEventTypes", () => {
});
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...evType,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
hidden: false,
hashedLink: undefined,
workflows: undefined,
scheduleId: undefined,
},
where: {
userId_parentId: {
@@ -37,6 +37,7 @@ export const getSampleUserInSession = function () {
disableImpersonation: false,
organizationId: null,
theme: "",
appTheme: "",
createdDate: new Date(),
trialEndsAt: new Date(),
completedOnboarding: false,
@@ -14,6 +14,7 @@ import closecom_config_json from "./closecom/config.json";
import cron_config_json from "./cron/config.json";
import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata";
import deel_config_json from "./deel/config.json";
import demodesk_config_json from "./demodesk/config.json";
import discord_config_json from "./discord/config.json";
import eightxeight_config_json from "./eightxeight/config.json";
import element_call_config_json from "./element-call/config.json";
@@ -34,6 +35,7 @@ import ics_feedcalendar_config_json from "./ics-feedcalendar/config.json";
import intercom_config_json from "./intercom/config.json";
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata";
import linear_config_json from "./linear/config.json";
import make_config_json from "./make/config.json";
import matomo_config_json from "./matomo/config.json";
import metapixel_config_json from "./metapixel/config.json";
@@ -96,6 +98,7 @@ export const appStoreMetadata = {
cron: cron_config_json,
dailyvideo: dailyvideo__metadata_ts,
deel: deel_config_json,
demodesk: demodesk_config_json,
discord: discord_config_json,
eightxeight: eightxeight_config_json,
"element-call": element_call_config_json,
@@ -116,6 +119,7 @@ export const appStoreMetadata = {
intercom: intercom_config_json,
jitsivideo: jitsivideo__metadata_ts,
larkcalendar: larkcalendar__metadata_ts,
linear: linear_config_json,
make: make_config_json,
matomo: matomo_config_json,
metapixel: metapixel_config_json,
@@ -14,6 +14,7 @@ export const apiHandlers = {
closecom: import("./closecom/api"),
cron: import("./cron/api"),
deel: import("./deel/api"),
demodesk: import("./demodesk/api"),
discord: import("./discord/api"),
eightxeight: import("./eightxeight/api"),
"element-call": import("./element-call/api"),
@@ -34,6 +35,7 @@ export const apiHandlers = {
intercom: import("./intercom/api"),
jitsivideo: import("./jitsivideo/api"),
larkcalendar: import("./larkcalendar/api"),
linear: import("./linear/api"),
make: import("./make/api"),
matomo: import("./matomo/api"),
metapixel: import("./metapixel/api"),
@@ -5,6 +5,7 @@
import around_config_json from "./around/config.json";
import campfire_config_json from "./campfire/config.json";
import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata";
import demodesk_config_json from "./demodesk/config.json";
import discord_config_json from "./discord/config.json";
import eightxeight_config_json from "./eightxeight/config.json";
import element_call_config_json from "./element-call/config.json";
@@ -41,6 +42,7 @@ export const appStoreMetadata = {
around: around_config_json,
campfire: campfire_config_json,
dailyvideo: dailyvideo__metadata_ts,
demodesk: demodesk_config_json,
discord: discord_config_json,
eightxeight: eightxeight_config_json,
"element-call": element_call_config_json,
@@ -0,0 +1,30 @@
---
items:
- iframe: { src: https://www.youtube.com/embed/X6XVzJ0E8xY }
- 1.jpeg
- 2.webp
- 3.webp
- 4.webp
---
{DESCRIPTION}
### AI superpowers for your sales meetings
#### Coach and onboard sellers with AI-based insights
- Automatically record all meetings, GDPR & MiFID compliant
- Transcribe and summarize meetings using Demodesk’s AI
- Coach teams along conversational metrics and scorecards
- Automatically log key information into your CRM
#### Schedule more customer meetings, on auto-pilot
- Let customers schedule time without email back-and-forth
- Qualify website leads and route them to the right person
- Automate and control handoff processes in your team
- Automate admin work and sync interactions to your CRM
#### Run secure video meetings that convert into revenue
- Host professional, secure video meetings without downloads
- Engage customers with interactive screen sharing
- Coach sellers in real-time along standardized playbooks
- Automate preparation, follow-up and CRM documentation
+16
View File
@@ -0,0 +1,16 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
};
export default handler;
+1
View File
@@ -0,0 +1 @@
export { default as add } from "./add";
+25
View File
@@ -0,0 +1,25 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Demodesk",
"slug": "demodesk",
"type": "demodesk_conferencing",
"logo": "icon.svg",
"url": "https://demodesk.com",
"variant": "conferencing",
"categories": ["conferencing"],
"publisher": "Cal.com, Inc.",
"email": "support@cal.com",
"appData": {
"location": {
"type": "integrations:{SLUG}_video",
"label": "{TITLE}",
"linkType": "static",
"organizerInputPlaceholder": "https://demodesk.com/meet/mylink",
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?demodesk.com\\/[a-zA-Z0-9]*"
}
},
"description": "Run Professional Video Meetings, Coach Sales Teams in Real-Time with AI, And Schedule Meetings on Auto-Pilot. 100% GDPR Compliant, Enterprise Ready.",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "event-type-location-video-static"
}
+1
View File
@@ -0,0 +1 @@
export * as api from "./api";
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/demodesk",
"version": "0.0.1",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "Run Professional Video Meetings, Coach Sales Teams in Real-Time with AI, And Schedule Meetings on Auto-Pilot. 100% GDPR Compliant, Enterprise Ready."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@@ -0,0 +1,4 @@
<svg width="2484" height="2484" viewBox="0 0 2484 2484" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="2484" height="2484" rx="200" fill="#DDDDDD"/>
<path d="M1126.24 514.004H687.083C663.823 514.004 645 532.827 645 556.088V656.4C645 679.661 663.823 698.484 687.083 698.484H1109.75C1451.81 698.484 1699.98 941.881 1699.98 1241.1C1699.98 1365.7 1657.75 1480.59 1586.85 1572.29C1599.97 1525.62 1607.02 1476.42 1607.02 1425.58C1607.02 1126.36 1418.26 882.963 1025.89 882.963H687.083C663.823 882.963 645 901.786 645 925.047V1025.36C645 1048.62 663.823 1067.44 687.083 1067.44H1019.19C1270.01 1068.59 1412.48 1228.09 1412.48 1425.58C1412.48 1623.06 1270.01 1783.75 1019.19 1783.75H850.096V1294.04C850.096 1270.79 831.274 1251.96 808.014 1251.96H687.083C663.823 1251.96 645 1270.79 645 1294.04V1926.15C645 1949.4 663.823 1968.23 687.083 1968.23H1064.45H1064.53H1126.28C1579.28 1968.23 1894.59 1642.04 1894.59 1241.13C1894.59 840.229 1607.05 512.703 1126.24 514.004Z" fill="#2B2D28"/>
</svg>

After

Width:  |  Height:  |  Size: 998 B

@@ -21,6 +21,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
switchChecked={enabled}
teamId={eventType.team?.id || undefined}>
<TextField
dataTestid={app.slug}
name="Tracking ID"
value={trackingId}
disabled={disabled}
@@ -21,6 +21,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
switchChecked={enabled}
teamId={eventType.team?.id || undefined}>
<TextField
dataTestid={app.slug}
name="Tracking ID"
value={trackingId}
disabled={disabled}
@@ -2,6 +2,7 @@ import type { Auth } from "googleapis";
import { google } from "googleapis";
import type { NextApiRequest, NextApiResponse } from "next";
import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar";
import { WEBAPP_URL_FOR_OAUTH, WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { HttpError } from "@calcom/lib/http-error";
@@ -105,23 +106,38 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
appId: "google-calendar",
},
});
const selectedCalendarWhereUnique = {
userId: req.session.user.id,
externalId: primaryCal.id,
integration: "google_calendar",
};
// Wrapping in a try/catch to reduce chance of race conditions-
// also this improves performance for most of the happy-paths.
try {
await prisma.selectedCalendar.create({
data: {
userId: req.session.user.id,
externalId: primaryCal.id,
credentialId: credential.id,
integration: "google_calendar",
...selectedCalendarWhereUnique,
},
});
} catch (error) {
await prisma.credential.delete({ where: { id: credential.id } });
let errorMessage = "something_went_wrong";
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
// it is possible a selectedCalendar was orphaned, in this situation-
// we want to recover by connecting the existing selectedCalendar to the new Credential.
if (await renewSelectedCalendarCredentialId(selectedCalendarWhereUnique, credential.id)) {
res.redirect(
getSafeRedirectUrl(state?.returnTo) ??
getInstalledAppPath({ variant: "calendar", slug: "google-calendar" })
);
return;
}
// else
errorMessage = "account_already_linked";
}
await prisma.credential.delete({ where: { id: credential.id } });
res.redirect(
`${
getSafeRedirectUrl(state?.onErrorReturnTo) ??
@@ -9,9 +9,7 @@ afterEach(() => {
});
vi.mock("@calcom/features/flags/server/utils", () => ({
getFeatureFlagMap: vi.fn().mockResolvedValue({
"calendar-cache": true,
}),
getFeatureFlag: vi.fn().mockReturnValue(true),
}));
vi.mock("./getGoogleAppKeys", () => ({
@@ -6,7 +6,7 @@ import { RRule } from "rrule";
import { MeetLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
import type CalendarService from "@calcom/lib/CalendarService";
import logger from "@calcom/lib/logger";
@@ -264,6 +264,7 @@ export default class GoogleCalendarService implements Calendar {
calendarId: selectedCalendar,
eventId: event.id || "",
requestBody: {
location: getLocation(calEventRaw),
description: getRichDescription({
...calEventRaw,
}),
@@ -461,10 +462,10 @@ export default class GoogleCalendarService implements Calendar {
items: { id: string }[];
}): Promise<EventBusyDate[] | null> {
const calendar = await this.authedCalendar();
const flags = await getFeatureFlagMap(prisma);
const calendarCacheEnabled = await getFeatureFlag(prisma, "calendar-cache");
let freeBusyResult: calendar_v3.Schema$FreeBusyResponse = {};
if (!flags["calendar-cache"]) {
if (!calendarCacheEnabled) {
this.log.warn("Calendar Cache is disabled - Skipping");
const { timeMin, timeMax, items } = args;
const apires = await calendar.freebusy.query({
@@ -21,6 +21,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
switchChecked={enabled}
teamId={eventType.team?.id || undefined}>
<TextField
dataTestid={app.slug}
name="Tracking ID"
value={trackingId}
disabled={disabled}
@@ -2,6 +2,7 @@
"name": "ICS Feed",
"title": "ICS Feed",
"slug": "ics-feed",
"dirName":"ics-feedcalendar",
"type": "ics-feed_calendar",
"logo": "icon.svg",
"variant": "calendar",
+19
View File
@@ -0,0 +1,19 @@
---
items:
- 1.jpg
---
{DESCRIPTION}
### Overview
The Linear integration with Google calendar allows you to connect your calendar and automate your status. If your teammates need to know whether you’re out of office, now they will.
### How it works
Once connected, your Linear status will detect out of office events in Google calendar and update your status when those events are upcoming or active.
### To Configure
Install Google Calendar from your Linear settings under My Account > Linked accounts.
Connect your account and sign into your Google account to allow access.
Success! Your calendar is connected and will update your Linear status with out of office notices.
+20
View File
@@ -0,0 +1,20 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
redirect: {
newTab: true,
url: "https://linear.app/settings/integrations/google-calendar",
},
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
};
export default handler;
+1
View File
@@ -0,0 +1 @@
export { default as add } from "./add";
+17
View File
@@ -0,0 +1,17 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Linear",
"slug": "linear",
"type": "linear_other",
"logo": "icon.svg",
"url": "https://linear.app",
"variant": "other",
"categories": ["other"],
"publisher": "Linear",
"email": "support@linear.app",
"description": "Linear is a better way to build products. Connect your calendar to automate your status",
"isTemplate": false,
"__createdUsingCli": true,
"dependencies": ["google-calendar"],
"__template": "link-as-an-app"
}
+1
View File
@@ -0,0 +1 @@
export * as api from "./api";
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/linear",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "Connect your calendar to automate your status"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+50
View File
@@ -0,0 +1,50 @@
<svg width="2484" height="2484" viewBox="0 0 2484 2484" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_981_40)">
<path d="M0 0H2484V2484H0V0Z" fill="url(#paint0_linear_981_40)"/>
<g filter="url(#filter0_dd_981_40)">
<path d="M636.219 1385.11C633.456 1373.33 647.489 1365.91 656.048 1374.47L1109.53 1827.95C1118.09 1836.51 1110.67 1850.54 1098.89 1847.78C870.04 1794.1 689.904 1613.96 636.219 1385.11Z" fill="url(#paint1_linear_981_40)"/>
<path d="M621.024 1203.36C620.804 1206.88 622.128 1210.32 624.619 1212.81L1271.19 1859.38C1273.68 1861.87 1277.12 1863.2 1280.64 1862.98C1310.06 1861.14 1338.93 1857.26 1367.11 1851.48C1376.6 1849.53 1379.9 1837.86 1373.05 1831.01L652.994 1110.95C646.138 1104.1 634.473 1107.4 632.522 1116.89C626.737 1145.07 622.856 1173.94 621.024 1203.36Z" fill="url(#paint2_linear_981_40)"/>
<path d="M673.3 989.942C671.233 994.585 672.286 1000.01 675.879 1003.6L1480.4 1808.12C1483.99 1811.71 1489.42 1812.77 1494.06 1810.7C1516.24 1800.82 1537.74 1789.68 1558.46 1777.36C1565.32 1773.29 1566.38 1763.87 1560.74 1758.23L725.772 923.262C720.132 917.622 710.71 918.68 706.635 925.537C694.322 946.258 683.18 967.758 673.3 989.942Z" fill="url(#paint3_linear_981_40)"/>
<path d="M778.222 845.479C773.625 840.882 773.339 833.508 777.671 828.661C891.501 701.225 1057.08 621 1241.4 621C1584.7 621 1863 899.298 1863 1242.6C1863 1426.92 1782.77 1592.5 1655.34 1706.33C1650.49 1710.66 1643.12 1710.37 1638.52 1705.78L778.222 845.479Z" fill="url(#paint4_linear_981_40)"/>
</g>
</g>
<defs>
<filter id="filter0_dd_981_40" x="585.357" y="590.926" width="1313.29" height="1313.29" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5.5693"/>
<feGaussianBlur stdDeviation="17.8218"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.118924 0 0 0 0 0.158031 0 0 0 0 0.570833 0 0 0 0.7 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_981_40"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1.94926"/>
<feGaussianBlur stdDeviation="7.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.17 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_981_40" result="effect2_dropShadow_981_40"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_981_40" result="shape"/>
</filter>
<linearGradient id="paint0_linear_981_40" x1="1242" y1="0" x2="1242" y2="2484" gradientUnits="userSpaceOnUse">
<stop stop-color="#5C6BF1"/>
<stop offset="1" stop-color="#283188"/>
</linearGradient>
<linearGradient id="paint1_linear_981_40" x1="665.358" y1="736.329" x2="1312.97" y2="1685.57" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.65"/>
</linearGradient>
<linearGradient id="paint2_linear_981_40" x1="665.358" y1="736.329" x2="1312.97" y2="1685.57" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.65"/>
</linearGradient>
<linearGradient id="paint3_linear_981_40" x1="665.358" y1="736.329" x2="1312.97" y2="1685.57" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.65"/>
</linearGradient>
<linearGradient id="paint4_linear_981_40" x1="665.358" y1="736.329" x2="1312.97" y2="1685.57" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.65"/>
</linearGradient>
<clipPath id="clip0_981_40">
<rect width="2484" height="2484" rx="200" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -23,6 +23,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
teamId={eventType.team?.id || undefined}>
<div className="flex flex-col gap-2">
<TextField
dataTestid={`${app.slug}-url`}
name="Matomo URL"
placeholder="Enter your Matomo URL here"
value={matomoUrl}
@@ -32,6 +33,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
}}
/>
<TextField
dataTestid={`${app.slug}-site-id`}
disabled={disabled}
name="Site ID"
placeholder="Enter your Site ID"

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