Compare commits

..
128 Commits
Author SHA1 Message Date
Omar López 844592e15e v3.8.7
release.yml / release (push) Canceled after 0s
2024-03-07 16:21:10 -07:00
Crowdin Bot bdacb18d5c New Crowdin translations by Github Action 2024-03-07 23:20:39 +00:00
1b5d50c45c feat: SCIM Group Provisioning To Teams Within An Org (#13850)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Kiran K <mailtokirankk@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-03-07 16:16:53 -07:00
Omar López 1c39dbad3d fix: avatar upload flakyness (#14021) 2024-03-07 19:14:06 +00:00
Alex van AndelandOmar López e60454750e fix: Implement new avatars in insights (#13996)
* fix: Implement new avatars in insights

* fix: Exposed type errors, previously hidden by any,any type

---------

Co-authored-by: Omar López <zomars@me.com>
2024-03-07 17:00:11 +00:00
Omar LópezandPeer Richelsen 286f696302 build: adds action to manually remove faulty cache entries (#14017)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-03-07 13:53:57 -03:00
Crowdin Bot 8deaea3edd New Crowdin translations by Github Action 2024-03-07 16:39:18 +00:00
Udit TakkarandErik 7d497a621f fix: duplicate event types (#14007)
* fix: duplicate event types

* test: add test for duplicate event type

* fix: type error

* chore: pass error message

---------

Co-authored-by: Erik <erik@erosemberg.com>
2024-03-07 16:34:42 +00:00
Somay Chauhan 5c8e0483b1 fix: add future team members toggle (#14008)
* fix: add future team members toggle

fix: add future team members toggle

* code refactor

* removed updateNewTeamMemberEventTypes for regularUsers
2024-03-07 16:18:45 +00:00
Omar López 3cefa94a94 Update base.json (#13997) 2024-03-07 11:38:40 -03:00
Kedar Choudharyandsean-brydon 30549d5c14 This commit fixes #13928 (#13962)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-03-07 14:07:33 +00:00
Crowdin Bot b87972399e New Crowdin translations by Github Action 2024-03-07 13:38:37 +00:00
sean-brydonandUdit Takkar 0784df6a31 fix: slugify from creating double+ dashes (#14002)
* fix: slugify

* fix: strip emojis

* fix: remove emojis

* fix: remove emojis

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-03-07 13:34:18 +00:00
fc062878ed add videoCallUrl from link based video apps to metadata (#13995)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-03-07 04:03:09 +00:00
Crowdin Bot 33c1d80a42 New Crowdin translations by Github Action 2024-03-06 23:38:10 +00:00
1064e1181d feat: SCIM 2.0 Automatic User provisioning (#11169)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-03-06 16:34:21 -07:00
Peer Richelsen dfd683ffc4 feat: added banner for orgs (#13644)
Use this banner to test https://pbs.twimg.com/profile_banners/1063089191565905920/1685635865/1500x500

- [x] add backend
- [x] add file upload (require 1500x500px) in Organization/Appearance
- [x] for now, apply this to all Org event types. later we could think about sub-team overwrites, but for now org is enough


1) Upload Banner Screen

<img width="1512" alt="Screenshot 2024-02-15 at 10 26 36 PM" src="https://github.com/calcom/cal.com/assets/53316345/3c7417ee-7ef7-4138-b5f8-38f255a407fa">

2) 

<img width="1512" alt="Screenshot 2024-02-15 at 10 27 02 PM" src="https://github.com/calcom/cal.com/assets/53316345/a74dffbf-3727-4b2f-82d5-fefd55c12901">


3) Go to Org Team Event Types

<img width="1512" alt="Screenshot 2024-02-15 at 10 27 21 PM" src="https://github.com/calcom/cal.com/assets/53316345/5f188bd7-8a66-4da7-be73-1b5e178b5b3b">


__________________________________________________

DESIGNS


![CleanShot 2024-02-12 at 15 02 37@2x](https://github.com/calcom/cal.com/assets/8019099/85602120-1ced-4627-ae7c-375d33ccf1e9)


__


![CleanShot 2024-02-12 at 14 55 23@2x](https://github.com/calcom/cal.com/assets/8019099/e8a9e9c3-4e01-4a07-a2e0-7259027feb36)


![CleanShot 2024-02-12 at 14 55 34@2x](https://github.com/calcom/cal.com/assets/8019099/56500809-6c6e-49a6-9229-43190dd711b7)

mobile:
![CleanShot 2024-02-12 at 14 56 00@2x](https://github.com/calcom/cal.com/assets/8019099/f90b58b2-3104-4c00-bc18-a2a54ac8e7e7)
2024-03-06 16:18:56 -07:00
Amit Sharma 0972307998 fix: unable to remove single recurring instance from gCal (#13960) 2024-03-06 13:46:58 -05:00
Keith Williams 157f633ef2 chore: Reduce insights range to 7 days (#13991) 2024-03-06 13:02:02 +00:00
Peer RichelsenandAlex van Andel cc1c3b210a chore: added new .env for script tags (#13765)
* added new .env for script tags

* Update turbo.json

* removed meticulous

* added body script .env

* nit

* fixed injection

* Update apps/web/app/layout.tsx

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Ensure NEXT_PUBLIC_BODY_SCRIPTS is casted to bool

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-03-06 12:00:19 +00:00
Crowdin Bot fcfd47e7d2 New Crowdin translations by Github Action 2024-03-06 10:14:02 +00:00
Udit Takkar 284c4c9084 fix: Email Embed for team event types (#13990)
* fix: embeds for team event types

* chore: remove unused
2024-03-06 07:05:39 -03:00
Swain MolsterandCarina Wollendorfer 9fd3594f6c fix: add the X-Cal-Signature-256 header to MEETING_ENDED webhook requests (#13986)
* add the X-Cal-Signature-256 header to MEETING_ENDED webhook requests

* take care of null value for appId

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-03-06 00:29:30 +00:00
Udit Takkar 531371bf55 fix: team event type error when when no one is assigned (#13888)
* fix: team event when when no one is assigned

* chore: test

* chore: unfo

* chore: remove host while creating

* fix: unit tests

* fix: event in booking form

* chore: undo tests

* fix: type

* fix: move it inside
2024-03-05 16:42:35 -03:00
Morgan 99fd4fe7a4 v3.8.6
release.yml / release (push) Canceled after 0s
2024-03-05 16:20:21 -03:00
a677d0d2c4 fix: Organization Migration - Prevent the redirected username from being claimed by others. (#13675)
* Check for migrated username

* Add e2e for reserved username due to migration

* Add username change test

* Update username.ts

* fixed type checks

* Refactor to use isOrganization

* Typing

* Typing

* Removed the isOrganization function

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-03-05 17:08:44 +00:00
Crowdin Bot d8cee20d0c New Crowdin translations by Github Action 2024-03-05 14:04:30 +00:00
a7ce12e22f fix: event type redirect for orgs in preview test (routing-forms) (#13686)
* fix event type url for orgs

* fix type errors

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-03-05 13:55:50 +00:00
Crowdin Bot 88b67a8121 New Crowdin translations by Github Action 2024-03-05 11:59:20 +00:00
Crowdin Bot 7e2a1a23b9 New Crowdin translations by Github Action 2024-03-05 11:55:45 +00:00
Crowdin Bot a115a3458e New Crowdin translations by Github Action 2024-03-05 11:51:32 +00:00
Crowdin Bot ae503acccc New Crowdin translations by Github Action 2024-03-05 11:48:07 +00:00
Crowdin Bot 61e7c89f36 New Crowdin translations by Github Action 2024-03-05 11:44:09 +00:00
Crowdin Bot 60ba7a5ca2 New Crowdin translations by Github Action 2024-03-05 11:40:01 +00:00
Crowdin Bot bfa718e193 New Crowdin translations by Github Action 2024-03-05 11:36:19 +00:00
Crowdin Bot b29a52d0b0 New Crowdin translations by Github Action 2024-03-05 11:31:48 +00:00
Crowdin Bot c21d9d08ab New Crowdin translations by Github Action 2024-03-05 11:27:28 +00:00
Crowdin Bot 615e1dcfe8 New Crowdin translations by Github Action 2024-03-05 11:23:48 +00:00
Crowdin Bot a24f9b76f6 New Crowdin translations by Github Action 2024-03-05 11:19:36 +00:00
Crowdin Bot 62f8ac1c6d New Crowdin translations by Github Action 2024-03-05 11:15:32 +00:00
PritamandJoe Au-Yeung 09b91d5a7e fix: Bug with recurring bookings with seats enabled (#13840)
* fix CAL-3165

* fix wrong change

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-03-05 11:09:35 +00:00
01ac2e640a feat: improvement org member list (#13859)
* fix: sticky action bar and align checkbox

* feat: make teams filterable and clickable, total members

* fix: update members count

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-03-05 11:09:14 +00:00
Udit TakkarandJoe Au-Yeung e39bf37f91 chore: remove CF-region (#13828)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-03-05 02:42:56 +05:30
Joe Au-YeungandKeith Williams 1f845944ac chore: Add credential sync secret when requesting a token (#13907)
* Rename .env variables

* Remove instances of old variable names

* Add credential sync secret to request for new token

* Update invalid secret message

Co-authored-by: Keith Williams <keithwillcode@gmail.com>

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-03-04 19:02:48 +00:00
Keith Williams 7f53db601d chore: Add index to Team.parentId (#13950) 2024-03-04 15:19:24 -03:00
Keith Williams 1cc9f1d25d chore: Increase API rate limit (#13970) 2024-03-04 16:44:39 +00:00
Udit Takkar c6ee482478 fix: email embed for org (#13917)
* fix: email embed for org

* chore: use orgslig

* chore: remove unused

* fix: org details

* test: add tests

* chore: type err

* fix: import missing

* fix: check timeslots
2024-03-04 15:48:13 +00:00
sean-brydonandAlex van Andel 5292dd2e66 fix: add org settings into seed (#13968)
* fix: add org settings into seed

* fix: Invalid seeded team event (missing schedulingType)

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-03-04 15:11:10 +00:00
Hiten Vats f59d62f03c refactor: Removed use of depracted functions from langchain (#13953) 2024-03-04 14:38:36 +00:00
Crowdin Bot cb02268e7f New Crowdin translations by Github Action 2024-03-04 14:09:47 +00:00
Peer Richelsen e33453c50d hide terms and privacy on self-hosted (#13966) 2024-03-04 14:04:18 +00:00
Crowdin Bot 15ffeec745 New Crowdin translations by Github Action 2024-03-04 12:48:39 +00:00
Crowdin Bot b7252ed79c New Crowdin translations by Github Action 2024-03-04 12:45:40 +00:00
Crowdin Bot 9b04df9bbe New Crowdin translations by Github Action 2024-03-04 12:41:18 +00:00
Crowdin Bot 657be16a4e New Crowdin translations by Github Action 2024-03-04 12:37:08 +00:00
Crowdin Bot b0afb15939 New Crowdin translations by Github Action 2024-03-04 12:33:30 +00:00
Vikas VermaandVikas Verma 3a9b8a9bf6 fix: url input field inconsistent (#13956)
* fix:Url Input feild Inconsistent UI/UX #13955

* Lint issue fixes

* Update UsernameTextfield.tsx

* Update UsernameTextfield.tsx

Lint issue fix

---------

Co-authored-by: Vikas Verma <inboxvikasverma@gmail.com>
2024-03-04 12:29:35 +00:00
92a919069d feat: Implement secondary email (#13678)
* Implement secondary email

* Fix already existing tests failing

* Added tests for secondary email feature

* Skip email verification if user is changing the primary email to a verified secondary email

* Fix type errors in tests

* Fix email becoming unverified when switched between primary and secondary email

* Added a check to prevent prisma error from throwing up due to duplicate records

* Improved error handling when adding a secondary email

* Add test for resend verification email flow for secondary emails and validation of invite link

* Add a new column to link secondary emails with verification tokens

* Fix failing to update email to an unverified secondary email of the same user

* Fix failing tests

* Change text of resend verification email

* Add ability to use the verified secondary emails to get the event details to

* Fix type errors

* Fix failing e2e tests

* Fix failing unit tests

* Fix failing type checks

* Fix secondary verification email subject

* Fix failing e2e tests

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-03-02 22:29:40 +00:00
zomars 0071d16575 Update yarn.lock 2024-03-01 13:26:00 -07:00
Joe Au-Yeung 23d22ee7ea Add missing lock file (#13949) 2024-03-01 14:28:10 -05:00
3a4f944aac feat: jelly integration (#13929)
* v1

* don't need to search DB for user

* return early if no user

* use one deleteMany to ensure only 1 credential

* rm expiration

* fix API request

* proper type

* Update packages/app-store/jelly/lib/VideoApiAdapter.ts

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* Update packages/app-store/jelly/lib/VideoApiAdapter.ts

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* Update packages/app-store/jelly/lib/VideoApiAdapter.ts

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* Update packages/app-store/jelly/api/callback.ts

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* Fix callback URL

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2024-03-01 16:45:41 +00:00
Crowdin Bot 3eadae0aad New Crowdin translations by Github Action 2024-03-01 15:53:12 +00:00
Crowdin Bot 0cc2fe2d7f New Crowdin translations by Github Action 2024-03-01 15:42:22 +00:00
0970c77009 fix: Revert -> "Revert "chore: Chore team metadata table + isOrganization migration (#13700)
* Revert "Revert "chore: Chore team metadata table + isOrganization migration from metadata (#12828)""

This reverts commit 2408338ed4.

* Remove constraint slug,isOrganization and reset migration

* fix: conflicts

* change schema to bust cache

* Fix issues reported by TS

* change schema to bust cache

* Review fixes

* Colaesce for orgAutoAcceptEmail as well

* Fix missing negation

---------

Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-03-01 15:04:28 +00:00
David Somers 5b733765f4 Update api image Dockerfile (#13890)
- Add DATABASE_URL and DATABASE_DIRECT_URL, used during build
- Copy tests folder, referenced by code run during build.
2024-03-01 09:18:02 -03:00
Yash RajandCarina Wollendorfer 83fe4d6625 fix: font bold font semibold (#13939)
* Fixed issue#13879 changed font-bold to font-semibold with font-cal

* Revert yarn.lock changes

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-03-01 06:53:16 -03:00
Joe Au-Yeungandzomars 91beeeb525 fix: Google Profile Picture Update Race Condition (#13711)
* Minor fixes

* Discard changes to packages/app-store/ics-feedcalendar/config.json

* Add comment

* refactor: denesting and conditional profile update

---------

Co-authored-by: zomars <zomars@me.com>
2024-02-29 20:01:13 +00:00
81c0b1ffe9 fix: broken google account avatar fix (#13892)
* fix: broken google account avatar fix

* fix: use zod for checking absolute url

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-02-29 19:59:23 +00:00
Hitesh SisaraandUdit Takkar aedc88f975 feat: show top banner for broken integrations [Cal 3125] (#13720)
* added InvalidAppCredentialBanner

* Create InvalidAppCredentialsBanner.tsx

* Update getUserTopBanners.handler.ts

* Create checkForInvalidAppCredentials.ts

* Update common.json

* Update common.json

* Update getUserTopBanners.handler.ts

* Update checkForInvalidAppCredentials.ts

* also included team's credentials

* reverse yarnlock changes

* fix .env

* fixed types

* chore: translation

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-02-29 19:58:46 +00:00
sean-brydonandOmar López 67256b5b6e fix: toLowercase on emailChangeWaitingForVerification (#13940)
Co-authored-by: Omar López <zomars@me.com>
2024-02-29 19:43:48 +00:00
Carina WollendorferandCarinaWolli ef02d64f90 fix: workfow e2e tests (#13938)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-02-29 11:16:36 -07:00
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
362 changed files with 8572 additions and 6454 deletions
+2 -2
View File
@@ -294,9 +294,9 @@ E2E_TEST_CALCOM_GCAL_KEYS=
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
# You can use: `openssl rand -base64 32` to generate one
CALCOM_WEBHOOK_SECRET=""
CALCOM_CREDENTIAL_SYNC_SECRET=""
# This is the header name that will be used to verify the webhook secret. Should be in lowercase
CALCOM_WEBHOOK_HEADER_NAME="calcom-webhook-secret"
CALCOM_CREDENTIAL_SYNC_SECRET_HEADER_NAME="calcom-webhook-secret"
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
# Key should match on Cal.com and your application
# must be 24 bytes for AES256 encryption algorithm
@@ -0,0 +1,17 @@
name: Manually Delete BuildJet Cache
on:
workflow_dispatch:
inputs:
cache_key:
description: "BuildJet Cache Key to Delete"
required: true
type: string
jobs:
manually-delete-buildjet-cache:
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: buildjet/cache-delete@v1
with:
cache_key: ${{ inputs.cache_key }}
+63 -57
View File
@@ -1,5 +1,6 @@
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { initializeAgentExecutorWithOptions } from "langchain/agents";
import { AgentExecutor, createOpenAIFunctionsAgent } from "langchain/agents";
import { env } from "../env.mjs";
import createBookingIfAvailable from "../tools/createBooking";
@@ -47,67 +48,72 @@ const agent = async (
/**
* Initialize the agent executor with arguments.
*/
const executor = await initializeAgentExecutorWithOptions(tools, model, {
agentArgs: {
prefix: `You are Cal.ai - a bleeding edge scheduling assistant that interfaces via email.
Make sure your final answers are definitive, complete and well formatted.
Sometimes, tools return errors. In this case, try to handle the error intelligently or ask the user for more information.
Tools will always handle times in UTC, but times sent to users should be formatted per that user's timezone.
In responses to users, always summarize necessary context and open the door to follow ups. For example "I have booked your chat with @username for 3pm on Wednesday, December 20th, 2023 EST. Please let me know if you need to reschedule."
If you can't find a referenced user, ask the user for their email or @username. Make sure to specify that usernames require the @username format. Users don't know other users' userIds.
const prompt =
ChatPromptTemplate.fromTemplate(`You are Cal.ai - a bleeding edge scheduling assistant that interfaces via email.
Make sure your final answers are definitive, complete and well formatted.
Sometimes, tools return errors. In this case, try to handle the error intelligently or ask the user for more information.
Tools will always handle times in UTC, but times sent to users should be formatted per that user's timezone.
In responses to users, always summarize necessary context and open the door to follow ups. For example "I have booked your chat with @username for 3pm on Wednesday, December 20th, 2023 EST. Please let me know if you need to reschedule."
If you can't find a referenced user, ask the user for their email or @username. Make sure to specify that usernames require the @username format. Users don't know other users' userIds.
The primary user's id is: ${userId}
The primary user's username is: ${user.username}
The current time in the primary user's timezone is: ${now(user.timeZone, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
})}
The primary user's time zone is: ${user.timeZone}
The primary user's event types are: ${user.eventTypes
.map((e: EventType) => `ID: ${e.id}, Slug: ${e.slug}, Title: ${e.title}, Length: ${e.length};`)
.join("\n")}
The primary user's working hours are: ${user.workingHours
.map(
(w: WorkingHours) =>
`Days: ${w.days.join(", ")}, Start Time (minutes in UTC): ${
w.startTime
}, End Time (minutes in UTC): ${w.endTime};`
)
.join("\n")}
${
users.length
? `The email references the following @usernames and emails: ${users
.map(
(u) =>
`${
(u.id ? `, id: ${u.id}` : "id: (non user)") +
(u.username
? u.type === "fromUsername"
? `, username: @${u.username}`
: ", username: REDACTED"
: ", (no username)") +
(u.email
? u.type === "fromEmail"
? `, email: ${u.email}`
: ", email: REDACTED"
: ", (no email)")
};`
)
.join("\n")}`
: ""
}`);
const agent = await createOpenAIFunctionsAgent({
llm: model,
prompt,
tools,
});
The primary user's id is: ${userId}
The primary user's username is: ${user.username}
The current time in the primary user's timezone is: ${now(user.timeZone, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
})}
The primary user's time zone is: ${user.timeZone}
The primary user's event types are: ${user.eventTypes
.map((e: EventType) => `ID: ${e.id}, Slug: ${e.slug}, Title: ${e.title}, Length: ${e.length};`)
.join("\n")}
The primary user's working hours are: ${user.workingHours
.map(
(w: WorkingHours) =>
`Days: ${w.days.join(", ")}, Start Time (minutes in UTC): ${
w.startTime
}, End Time (minutes in UTC): ${w.endTime};`
)
.join("\n")}
${
users.length
? `The email references the following @usernames and emails: ${users
.map(
(u) =>
`${
(u.id ? `, id: ${u.id}` : "id: (non user)") +
(u.username
? u.type === "fromUsername"
? `, username: @${u.username}`
: ", username: REDACTED"
: ", (no username)") +
(u.email
? u.type === "fromEmail"
? `, email: ${u.email}`
: ", email: REDACTED"
: ", (no email)")
};`
)
.join("\n")}`
: ""
}
`,
},
agentType: "openai-functions",
const executor = new AgentExecutor({
agent,
tools,
returnIntermediateSteps: env.NODE_ENV === "development",
verbose: env.NODE_ENV === "development",
});
const result = await executor.call({ input });
const result = await executor.invoke({ input });
const { output } = result;
return output;
+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,
+17 -7
View File
@@ -2,11 +2,9 @@ import { dir } from "i18next";
import { Inter } from "next/font/google";
import localFont from "next/font/local";
import { headers, cookies } from "next/headers";
import Script from "next/script";
import React from "react";
import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { prepareRootMetadata } from "@lib/metadata";
@@ -18,6 +16,7 @@ const calFont = localFont({
variable: "--font-cal",
preload: true,
display: "block",
weight: "600",
});
export const generateMetadata = () =>
@@ -69,11 +68,13 @@ export default async function RootLayout({ children }: { children: React.ReactNo
style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}
data-nextjs-router="app">
<head nonce={nonce}>
{!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
// eslint-disable-next-line @next/next/no-sync-scripts
<Script
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4"
src="https://snippet.meticulous.ai/v1/stagingMeticulousSnippet.js"
{!!process.env.NEXT_PUBLIC_HEAD_SCRIPTS && (
<script
nonce={nonce}
id="injected-head-scripts"
dangerouslySetInnerHTML={{
__html: process.env.NEXT_PUBLIC_HEAD_SCRIPTS,
}}
/>
)}
<style>{`
@@ -97,6 +98,15 @@ export default async function RootLayout({ children }: { children: React.ReactNo
}
: {}
}>
{!!process.env.NEXT_PUBLIC_BODY_SCRIPTS && (
<script
nonce={nonce}
id="injected-head-scripts"
dangerouslySetInnerHTML={{
__html: process.env.NEXT_PUBLIC_BODY_SCRIPTS,
}}
/>
)}
{children}
</body>
</html>
+1
View File
@@ -24,6 +24,7 @@ const calFont = localFont({
variable: "--font-cal",
preload: true,
display: "swap",
weight: "600",
});
function PageWrapper(props: AppProps) {
+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);
@@ -27,8 +27,10 @@ import { trpc } from "@calcom/trpc/react";
import {
Alert,
Button,
Badge,
CheckboxField,
Label,
SelectField,
SettingsToggle,
showToast,
Switch,
@@ -122,6 +124,16 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const closeEventNameTip = () => setShowEventNameTip(false);
const displayDestinationCalendarSelector =
!!connectedCalendarsQuery.data?.connectedCalendars.length && !team;
const verifiedSecondaryEmails = [
{
label: user?.email || "",
value: -1,
},
...(user?.secondaryEmails || [])
.filter((secondaryEmail) => !!secondaryEmail.emailVerified)
.map((secondaryEmail) => ({ label: secondaryEmail.email, value: secondaryEmail.id })),
];
const selectedSecondaryEmailId = formMethods.getValues("secondaryEmailId") || -1;
return (
<div className="flex flex-col space-y-4">
@@ -198,6 +210,28 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
/>
</div>
)}
{!useEventTypeDestinationCalendarEmail && verifiedSecondaryEmails.length > 0 && (
<div className="w-full">
<SelectField
label={t("send_event_details_to")}
placeholder={
selectedSecondaryEmailId === -1 && (
<span className="text-default min-w-0 overflow-hidden truncate whitespace-nowrap">
<Badge variant="blue">{t("default")}</Badge> {user?.email || ""}
</span>
)
}
onChange={(option) =>
formMethods.setValue("secondaryEmailId", option?.value, { shouldDirty: true })
}
value={verifiedSecondaryEmails.find(
(secondaryEmail) =>
selectedSecondaryEmailId !== -1 && secondaryEmail.value === selectedSecondaryEmailId
)}
options={verifiedSecondaryEmails}
/>
</div>
)}
</div>
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} isOuterBorder={true} />
@@ -360,10 +360,12 @@ export const EventLimitsTab = () => {
title={t("limit_future_bookings")}
description={t("limit_future_bookings_description")}
checked={isChecked}
onCheckedChange={(bool) =>
// formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED", { shouldDirty: true })
onChange(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}
@@ -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"
@@ -205,6 +205,7 @@ function EventTypeSingleLayout({
activeWebhooksNumber,
}: Props) {
const { t } = useLocale();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const hasPermsToDelete =
@@ -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");
@@ -0,0 +1,127 @@
import type { FormValues } from "@pages/settings/my-account/profile";
import { useState } from "react";
import type { UseFormReturn } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import {
Badge,
TextField,
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Button,
InputError,
} from "@calcom/ui";
import { MoreHorizontal, Flag, Trash, Send } from "@calcom/ui/components/icon";
type CustomEmailTextFieldProps = {
formMethods: UseFormReturn<FormValues>;
formMethodFieldName: keyof FormValues;
errorMessage: string;
emailVerified: boolean;
emailPrimary: boolean;
dataTestId: string;
handleChangePrimary: () => void;
handleVerifyEmail: () => void;
handleItemDelete: () => void;
};
const CustomEmailTextField = ({
formMethods,
formMethodFieldName,
errorMessage,
emailVerified,
emailPrimary,
dataTestId,
handleChangePrimary,
handleVerifyEmail,
handleItemDelete,
}: CustomEmailTextFieldProps) => {
const { t } = useLocale();
const [inputFocus, setInputFocus] = useState(false);
return (
<>
<div
className={`border-default mt-2 flex items-center rounded-md border ${
inputFocus ? "ring-brand-default border-neutral-300 ring-2" : ""
}`}>
<TextField
{...formMethods.register(formMethodFieldName)}
label=""
containerClassName="flex flex-1 items-center"
className="mb-0 border-none outline-none focus:ring-0"
data-testid={dataTestId}
onFocus={() => setInputFocus(true)}
onBlur={() => setInputFocus(false)}
/>
<div className="flex items-center pr-2">
{emailPrimary && (
<Badge variant="blue" size="sm" data-testid={`${dataTestId}-primary-badge`}>
{t("primary")}
</Badge>
)}
{!emailVerified && (
<Badge variant="orange" size="sm" className="ml-2" data-testid={`${dataTestId}-unverified-badge`}>
{t("unverified")}
</Badge>
)}
<Dropdown>
<DropdownMenuTrigger asChild>
<Button
StartIcon={MoreHorizontal}
variant="icon"
size="sm"
color="secondary"
className="ml-2 rounded-md"
data-testid="secondary-email-action-group-button"
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownItem
StartIcon={Flag}
color="secondary"
className="disabled:opacity-40"
onClick={handleChangePrimary}
disabled={!emailVerified || emailPrimary}
data-testid="secondary-email-make-primary-button">
{t("make_primary")}
</DropdownItem>
</DropdownMenuItem>
{!emailVerified && (
<DropdownMenuItem>
<DropdownItem
StartIcon={Send}
color="secondary"
className="disabled:opacity-40"
onClick={handleVerifyEmail}
disabled={emailVerified}
data-testid="resend-verify-email-button">
{t("resend_email")}
</DropdownItem>
</DropdownMenuItem>
)}
<DropdownMenuItem>
<DropdownItem
StartIcon={Trash}
color="destructive"
className="disabled:opacity-40"
onClick={handleItemDelete}
disabled={emailPrimary}
data-testid="secondary-email-delete-button">
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
</div>
{errorMessage && <InputError message={errorMessage} />}
</>
);
};
export default CustomEmailTextField;
@@ -0,0 +1,31 @@
import { Trans } from "react-i18next";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui";
interface SecondaryEmailConfirmModalProps {
email: string;
onCancel: () => void;
}
const SecondaryEmailConfirmModal = ({ email, onCancel }: SecondaryEmailConfirmModalProps) => {
const { t } = useLocale();
return (
<Dialog open={true}>
<DialogContent
title={t("confirm_email")}
description={<Trans i18nKey="confirm_email_description" values={{ email }} />}
type="creation"
data-testid="secondary-email-confirm-dialog">
<DialogFooter>
<DialogClose color="primary" onClick={onCancel} data-testid="secondary-email-confirm-done-button">
{t("done")}
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default SecondaryEmailConfirmModal;
@@ -0,0 +1,77 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import {
Dialog,
DialogContent,
DialogFooter,
DialogClose,
Button,
TextField,
Form,
InputError,
} from "@calcom/ui";
interface SecondaryEmailModalProps {
isLoading: boolean;
errorMessage?: string;
handleAddEmail: (value: { email: string }) => void;
onCancel: () => void;
clearErrorMessage: () => void;
}
const SecondaryEmailModal = ({
isLoading,
errorMessage,
handleAddEmail,
onCancel,
clearErrorMessage,
}: SecondaryEmailModalProps) => {
const { t } = useLocale();
type FormValues = {
email: string;
};
const formMethods = useForm<FormValues>({
resolver: zodResolver(
z.object({
email: z.string().email(),
})
),
});
useEffect(() => {
// We will reset the errorMessage once the user starts modifying the email
const subscription = formMethods.watch(() => clearErrorMessage());
return () => subscription.unsubscribe();
}, [formMethods.watch]);
return (
<Dialog open={true}>
<DialogContent
title={t("add_email")}
description={t("add_email_description")}
type="creation"
data-testid="secondary-email-add-dialog">
<Form form={formMethods} handleSubmit={handleAddEmail}>
<TextField
label={t("email_address")}
data-testid="secondary-email-input"
{...formMethods.register("email")}
/>
{errorMessage && <InputError message={errorMessage} />}
<DialogFooter showDivider className="mt-10">
<DialogClose onClick={onCancel}>{t("cancel")}</DialogClose>
<Button type="submit" data-testid="add-secondary-email-button" disabled={isLoading}>
{t("add_email")}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};
export default SecondaryEmailModal;
@@ -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,10 @@ 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 min-h-5 mt-1 max-h-12 overflow-y-auto 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;
+16 -10
View File
@@ -696,7 +696,7 @@ describe("orgMigration", () => {
});
describe("when user email matches orgAutoAcceptEmail", () => {
const orgMetadata = {
const orgSettings = {
orgAutoAcceptEmail: "org1.com",
} as const;
@@ -725,8 +725,10 @@ describe("orgMigration", () => {
const dbOrg = await createOrg({
slug: data.targetOrg.slug,
name: data.targetOrg.name,
metadata: {
...orgMetadata,
organizationSettings: {
create: {
...orgSettings,
},
},
});
@@ -784,8 +786,10 @@ describe("orgMigration", () => {
const dbOrg = await createOrg({
slug: data.targetOrg.slug,
name: data.targetOrg.name,
metadata: {
...orgMetadata,
organizationSettings: {
create: {
...orgSettings,
},
},
});
@@ -837,7 +841,11 @@ describe("orgMigration", () => {
name: data.targetOrg.name,
metadata: {
requestedSlug: data.targetOrg.requestedSlug,
...orgMetadata,
},
organizationSettings: {
create: {
...orgSettings,
},
},
});
@@ -901,9 +909,7 @@ describe("orgMigration", () => {
id: data.targetOrg.id,
slug: data.targetOrg.slug,
name: data.targetOrg.name,
metadata: {
isOrganization: true,
},
isOrganization: true,
},
});
@@ -1564,9 +1570,9 @@ async function createOrg(
return await prismock.team.create({
data: {
...data,
isOrganization: true,
metadata: {
...(data.metadata || {}),
isOrganization: true,
},
},
});
+14 -8
View File
@@ -47,7 +47,7 @@ export async function moveUserToOrg({
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
if (!teamMetadata?.isOrganization) {
if (!team.isOrganization) {
throw new Error(`Team with ID:${targetOrgId} is not an Org`);
}
@@ -61,7 +61,7 @@ export async function moveUserToOrg({
if (!targetOrgUsername) {
targetOrgUsername = getOrgUsernameFromEmail(
userToMoveToOrg.email,
targetOrganization.metadata.orgAutoAcceptEmail || ""
targetOrganization.organizationSettings?.orgAutoAcceptEmail || ""
);
}
@@ -196,7 +196,7 @@ export async function moveTeamToOrg({
const teamMetadata = teamMetadataSchema.parse(possibleOrg?.metadata);
if (!teamMetadata?.isOrganization) {
if (!possibleOrg.isOrganization) {
throw new Error(`${targetOrg.id} is not an Org`);
}
@@ -205,7 +205,7 @@ export async function moveTeamToOrg({
await addTeamRedirect({
oldTeamSlug,
teamSlug: updatedTeam.slug,
orgSlug: targetOrganization.slug || orgMetadata.requestedSlug || null,
orgSlug: targetOrganization.slug || orgMetadata?.requestedSlug || null,
});
await setOrgSlugIfNotSet({ slug: targetOrganization.slug }, orgMetadata, targetOrg.id);
if (moveMembers) {
@@ -329,13 +329,14 @@ async function setOrgSlugIfNotSet(
},
orgMetadata: {
requestedSlug?: string | null | undefined;
},
} | null,
targetOrgId: number
) {
if (targetOrganization.slug) {
return;
}
if (!orgMetadata.requestedSlug) {
if (!orgMetadata?.requestedSlug) {
throw new HttpError({
statusCode: 400,
message: `Org with id: ${targetOrgId} doesn't have a slug. Tried using requestedSlug but that's also not present. So, all migration done but failed to set the Organization slug. Please set it manually`,
@@ -372,6 +373,9 @@ async function getTeamOrThrowError(targetOrgId: number) {
where: {
id: targetOrgId,
},
include: {
organizationSettings: true,
},
});
if (!team) {
@@ -650,6 +654,7 @@ async function moveTeamsWithoutMembersToOrg({
id: true,
slug: true,
metadata: true,
isOrganization: true,
},
});
@@ -661,7 +666,7 @@ async function moveTeamsWithoutMembersToOrg({
};
})
// Remove Orgs from the list
.filter((team) => !team.metadata?.isOrganization);
.filter((team) => !team.isOrganization);
const teamIdsToBeMovedToOrg = teamsToBeMovedToOrg.map((t) => t.id);
@@ -814,6 +819,7 @@ async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { use
id: true,
slug: true,
metadata: true,
isOrganization: true,
},
});
@@ -825,7 +831,7 @@ async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { use
};
})
// Remove Orgs from the list
.filter((team) => !team.metadata?.isOrganization);
.filter((team) => !team.isOrganization);
const teamIdsToBeRemovedFromOrg = teamsToBeRemovedFromOrg.map((t) => t.id);
@@ -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;
+23 -11
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;
@@ -67,14 +78,17 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
team: {
select: {
metadata: true,
isOrganization: true,
parentId: true,
parent: {
select: {
slug: true,
metadata: true,
isOrganization: true,
organizationSettings: true,
},
},
slug: true,
organizationSettings: true,
},
},
},
@@ -123,8 +137,8 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
};
const isATeamInOrganization = tokenTeam?.parentId !== null;
const isOrganization = tokenTeam.metadata?.isOrganization;
// Detect if the team is an org by either the metadata flag or if it has a parent team
const isOrganization = tokenTeam.isOrganization;
const isOrganizationOrATeamInOrganization = isOrganization || isATeamInOrganization;
// If we are dealing with an org, the slug may come from the team itself or its parent
const orgSlug = isOrganizationOrATeamInOrganization
@@ -141,9 +155,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const isValidEmail = checkValidEmail(verificationToken.identifier);
const isOrgInviteByLink = isOrganizationOrATeamInOrganization && !isValidEmail;
const parentMetaDataForSubteam = tokenTeam?.parent?.metadata
? teamMetadataSchema.parse(tokenTeam.parent.metadata)
: null;
const parentOrgSettings = tokenTeam?.parent?.organizationSettings ?? null;
return {
props: {
@@ -156,15 +168,15 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
? getOrgUsernameFromEmail(
verificationToken.identifier,
(isOrganization
? tokenTeam.metadata?.orgAutoAcceptEmail
: parentMetaDataForSubteam?.orgAutoAcceptEmail) || ""
? tokenTeam.organizationSettings?.orgAutoAcceptEmail
: parentOrgSettings?.orgAutoAcceptEmail) || ""
)
: slugify(username),
}
: null,
orgSlug,
orgAutoAcceptEmail: isOrgInviteByLink
? tokenTeam?.metadata?.orgAutoAcceptEmail ?? parentMetaDataForSubteam?.orgAutoAcceptEmail ?? null
? tokenTeam?.organizationSettings?.orgAutoAcceptEmail ?? parentOrgSettings?.orgAutoAcceptEmail ?? null
: null,
},
};
@@ -96,6 +96,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
isBrandingHidden: team?.hideBranding,
isInstantMeeting: eventData.isInstantEvent && queryIsInstantMeeting ? true : false,
themeBasis: null,
orgBannerUrl: eventData?.team?.parent?.bannerUrl ?? "",
},
};
};
@@ -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,
});
@@ -73,8 +72,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
// Taking care of sub-teams and orgs
if (
(!isValidOrgDomain && team?.parent) ||
(!isValidOrgDomain && !!metadata?.isOrganization) ||
!isOrganizationFeatureEnabled
(!isValidOrgDomain && !!team?.isOrganization) ||
!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}
/>
</>
))}
@@ -149,6 +149,7 @@ export type FormValues = {
users: EventTypeSetup["users"];
assignAllTeamMembers: boolean;
useEventTypeDestinationCalendarEmail: boolean;
secondaryEmailId?: number;
};
export type CustomInputParsed = typeof customInputSchema._output;
@@ -266,7 +267,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,
@@ -302,6 +303,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
successRedirectUrl: eventType.successRedirectUrl || "",
users: eventType.users,
useEventTypeDestinationCalendarEmail: eventType.useEventTypeDestinationCalendarEmail,
secondaryEmailId: eventType?.secondaryEmailId || -1,
children: eventType.children.map((ch) => ({
...ch,
created: true,
@@ -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,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.8.1",
"version": "3.8.7",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+52 -2
View File
@@ -10,6 +10,8 @@ const verifySchema = z.object({
token: z.string(),
});
const USER_ALREADY_EXISTING_MESSAGE = "A User already exists with this email";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { token } = verifySchema.parse(req.query);
@@ -27,6 +29,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(401).json({ message: "Token expired" });
}
// The user is verifying the secondary email
if (foundToken?.secondaryEmailId) {
await prisma.secondaryEmail.update({
where: {
id: foundToken.secondaryEmailId,
email: foundToken?.identifier,
},
data: {
emailVerified: new Date(),
},
});
await cleanUpVerificationTokens(foundToken.id);
return res.redirect(`${WEBAPP_URL}/settings/my-account/profile`);
}
const user = await prisma.user.findFirst({
where: {
email: foundToken?.identifier,
@@ -49,9 +68,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
if (existingUser) {
return res.status(401).json({ message: "A User already exists with this email" });
return res.status(401).json({ message: USER_ALREADY_EXISTING_MESSAGE });
}
// Ensure this email isn't being added by another user as secondary email
const existingSecondaryUser = await prisma.secondaryEmail.findUnique({
where: {
email: userMetadataParsed?.emailChangeWaitingForVerification,
},
select: {
id: true,
userId: true,
},
});
if (existingSecondaryUser && existingSecondaryUser.userId !== user.id) {
return res.status(401).json({ message: USER_ALREADY_EXISTING_MESSAGE });
}
const oldEmail = user.email;
const updatedEmail = userMetadataParsed.emailChangeWaitingForVerification;
delete userMetadataParsed.emailChangeWaitingForVerification;
@@ -66,6 +101,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
// The user is trying to update the email to an already existing unverified secondary email of his
// so we swap the emails and its verified status
if (existingSecondaryUser?.userId === user.id) {
await prisma.secondaryEmail.update({
where: {
id: existingSecondaryUser.id,
userId: user.id,
},
data: {
email: oldEmail,
emailVerified: user.emailVerified,
},
});
}
await cleanUpVerificationTokens(foundToken.id);
return res.status(200).json({
@@ -87,7 +137,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.redirect(`${WEBAPP_URL}/${hasCompletedOnboarding ? "/event-types" : "/getting-started"}`);
}
async function cleanUpVerificationTokens(id: number) {
export async function cleanUpVerificationTokens(id: number) {
// Delete token from DB after it has been used
await prisma.verificationToken.delete({
where: {
@@ -0,0 +1,73 @@
import type { DirectorySyncEvent, DirectorySyncRequest } from "@boxyhq/saml-jackson";
import type { NextApiRequest, NextApiResponse } from "next";
import handleGroupEvents from "@calcom/features/ee/dsync/lib/handleGroupEvents";
import handleUserEvents from "@calcom/features/ee/dsync/lib/handleUserEvents";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import prisma from "@calcom/prisma";
// This is the handler for the SCIM API requests
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { dsyncController } = await jackson();
const { method, query, body } = req;
const [directoryId, path, resourceId] = query.directory as string[];
// Handle the SCIM API requests
const request: DirectorySyncRequest = {
method: method as string,
directoryId,
resourceId,
apiSecret: extractAuthToken(req),
resourceType: path === "Users" ? "users" : "groups",
body: body ? JSON.parse(body) : undefined,
query: {
count: req.query.count ? parseInt(req.query.count as string) : undefined,
startIndex: req.query.startIndex ? parseInt(req.query.startIndex as string) : undefined,
filter: req.query.filter as string,
},
};
const { status, data } = await dsyncController.requests.handle(request, handleEvents);
res.status(status).json(data);
}
// Fetch the auth token from the request headers
export const extractAuthToken = (req: NextApiRequest): string | null => {
const authHeader = req.headers.authorization || null;
return authHeader ? authHeader.split(" ")[1] : null;
};
// Handle the SCIM events
const handleEvents = async (event: DirectorySyncEvent) => {
const dSyncData = await prisma.dSyncData.findFirst({
where: {
directoryId: event.directory_id,
},
select: {
id: true,
organizationId: true,
},
});
if (!dSyncData) {
throw new Error("Directory sync data not found");
}
const { organizationId } = dSyncData;
if (!organizationId) {
throw new Error(`Org ID not found for dsync ${dSyncData.id}`);
}
if (event.event.includes("group")) {
handleGroupEvents(event, organizationId);
}
if (event.event === "user.created" || event.event === "user.updated") {
await handleUserEvents(event, organizationId);
}
};
+4
View File
@@ -0,0 +1,4 @@
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
import { dsyncRouter } from "@calcom/trpc/server/routers/viewer/dsync/_router";
export default createNextApiHandler(dsyncRouter);
+3 -5
View File
@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { CREDENTIAL_SYNC_SECRET, CREDENTIAL_SYNC_SECRET_HEADER_NAME } from "@calcom/lib/constants";
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
@@ -19,11 +20,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(403).json({ message: "Credential sharing is not enabled" });
}
if (
req.headers[process.env.CALCOM_WEBHOOK_HEADER_NAME || "calcom-webhook-secret"] !==
process.env.CALCOM_WEBHOOK_SECRET
) {
return res.status(403).json({ message: "Invalid webhook secret" });
if (req.headers[CREDENTIAL_SYNC_SECRET_HEADER_NAME] !== CREDENTIAL_SYNC_SECRET) {
return res.status(403).json({ message: "Invalid credential sync secret" });
}
const reqBody = appCredentialWebhookRequestBodySchema.parse(req.body);
@@ -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>
+183 -15
View File
@@ -1,10 +1,12 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
// eslint-disable-next-line no-restricted-imports
import { pick, get } from "lodash";
import { signOut, useSession } from "next-auth/react";
import type { BaseSyntheticEvent } from "react";
import React, { useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Controller, useForm, useFieldArray } from "react-hook-form";
import { z } from "zod";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
@@ -43,10 +45,13 @@ import {
TextField,
} from "@calcom/ui";
import { UserAvatar } from "@calcom/ui";
import { AlertTriangle, Trash2 } from "@calcom/ui/components/icon";
import { AlertTriangle, Trash2, Plus } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
import TwoFactor from "@components/auth/TwoFactor";
import CustomEmailTextField from "@components/settings/CustomEmailTextField";
import SecondaryEmailConfirmModal from "@components/settings/SecondaryEmailConfirmModal";
import SecondaryEmailModal from "@components/settings/SecondaryEmailModal";
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
@@ -72,12 +77,20 @@ interface DeleteAccountValues {
totpCode: string;
}
type FormValues = {
type Email = {
id: number;
email: string;
emailVerified: string | null;
emailPrimary: boolean;
};
export type FormValues = {
username: string;
avatar: string;
name: string;
email: string;
bio: string;
secondaryEmails: Email[];
};
const ProfileView = () => {
@@ -126,11 +139,27 @@ const ProfileView = () => {
},
});
const addSecondaryEmailMutation = trpc.viewer.addSecondaryEmail.useMutation({
onSuccess: (res) => {
setShowSecondaryEmailModalOpen(false);
setNewlyAddedSecondaryEmail(res?.data?.email);
utils.viewer.me.invalidate();
},
onError: (error) => {
setSecondaryEmailAddErrorMessage(error?.message || "");
},
});
const resendVerifyEmailMutation = trpc.viewer.auth.resendVerifyEmail.useMutation();
const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false);
const [tempFormValues, setTempFormValues] = useState<FormValues | null>(null);
const [tempFormValues, setTempFormValues] = useState<ExtendedFormValues | null>(null);
const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState("");
const [confirmAuthEmailChangeWarningDialogOpen, setConfirmAuthEmailChangeWarningDialogOpen] =
useState(false);
const [showSecondaryEmailModalOpen, setShowSecondaryEmailModalOpen] = useState(false);
const [secondaryEmailAddErrorMessage, setSecondaryEmailAddErrorMessage] = useState("");
const [newlyAddedSecondaryEmail, setNewlyAddedSecondaryEmail] = useState<undefined | string>(undefined);
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
@@ -232,6 +261,7 @@ const ProfileView = () => {
);
}
const userEmail = user.email || "";
const defaultValues = {
username: user.username || "",
avatar: getUserAvatarUrl({
@@ -239,8 +269,22 @@ const ProfileView = () => {
profile: user.profile,
}),
name: user.name || "",
email: user.email || "",
email: userEmail,
bio: user.bio || "",
// We add the primary email as the first item in the list
secondaryEmails: [
{
id: 0,
email: userEmail,
emailVerified: user.emailVerified?.toString() || null,
emailPrimary: true,
},
...(user.secondaryEmails || []).map((secondaryEmail) => ({
...secondaryEmail,
emailVerified: secondaryEmail.emailVerified?.toString() || null,
emailPrimary: false,
})),
],
};
return (
@@ -269,6 +313,11 @@ const ProfileView = () => {
updateProfileMutation.mutate(values);
}
}}
handleAddSecondaryEmail={() => setShowSecondaryEmailModalOpen(true)}
handleResendVerifyEmail={(email) => {
resendVerifyEmailMutation.mutate({ email });
showToast(t("email_sent"), "success");
}}
extraField={
<div className="mt-6">
<UsernameAvailabilityField
@@ -374,7 +423,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)}>
@@ -405,13 +454,50 @@ const ProfileView = () => {
</DialogFooter>
</DialogContent>
</Dialog>
{showSecondaryEmailModalOpen && (
<SecondaryEmailModal
isLoading={addSecondaryEmailMutation.isPending}
errorMessage={secondaryEmailAddErrorMessage}
handleAddEmail={(values) => {
setSecondaryEmailAddErrorMessage("");
addSecondaryEmailMutation.mutate(values);
}}
onCancel={() => {
setSecondaryEmailAddErrorMessage("");
setShowSecondaryEmailModalOpen(false);
}}
clearErrorMessage={() => {
addSecondaryEmailMutation.reset();
setSecondaryEmailAddErrorMessage("");
}}
/>
)}
{!!newlyAddedSecondaryEmail && (
<SecondaryEmailConfirmModal
email={newlyAddedSecondaryEmail}
onCancel={() => setNewlyAddedSecondaryEmail(undefined)}
/>
)}
</>
);
};
type SecondaryEmailApiPayload = {
id: number;
email: string;
isDeleted: boolean;
};
type ExtendedFormValues = Omit<FormValues, "secondaryEmails"> & {
secondaryEmails: SecondaryEmailApiPayload[];
};
const ProfileForm = ({
defaultValues,
onSubmit,
handleAddSecondaryEmail,
handleResendVerifyEmail,
extraField,
isPending = false,
isFallbackImg,
@@ -419,7 +505,9 @@ const ProfileForm = ({
userOrganization,
}: {
defaultValues: FormValues;
onSubmit: (values: FormValues) => void;
onSubmit: (values: ExtendedFormValues) => void;
handleAddSecondaryEmail: () => void;
handleResendVerifyEmail: (email: string) => void;
extraField?: React.ReactNode;
isPending: boolean;
isFallbackImg: boolean;
@@ -441,6 +529,14 @@ const ProfileForm = ({
}),
email: z.string().email(),
bio: z.string(),
secondaryEmails: z.array(
z.object({
id: z.number(),
email: z.string().email(),
emailVerified: z.union([z.string(), z.null()]).optional(),
emailPrimary: z.boolean().optional(),
})
),
});
const formMethods = useForm<FormValues>({
@@ -448,13 +544,61 @@ const ProfileForm = ({
resolver: zodResolver(profileFormSchema),
});
const {
fields: secondaryEmailFields,
remove: deleteSecondaryEmail,
replace: updateAllSecondaryEmailFields,
} = useFieldArray({
control: formMethods.control,
name: "secondaryEmails",
keyName: "itemId",
});
const handleFormSubmit = (values: FormValues) => {
const changedFields = formMethods.formState.dirtyFields?.secondaryEmails || [];
const updatedValues: FormValues = {
...values,
};
// If the primary email is changed, we will need to update
const primaryEmailIndex = updatedValues.secondaryEmails.findIndex(
(secondaryEmail) => secondaryEmail.emailPrimary
);
if (primaryEmailIndex >= 0) {
// Add the new updated value as primary email
updatedValues.email = updatedValues.secondaryEmails[primaryEmailIndex].email;
}
// We will only send the emails which have already changed
const updatedEmails: Email[] = [];
changedFields.map((field, index) => {
// If the email changed and if its only secondary email, we add it for updation, the first
// item in the list is always primary email
if (field?.email && updatedValues.secondaryEmails[index]?.id) {
updatedEmails.push(updatedValues.secondaryEmails[index]);
}
});
const deletedEmails = (user?.secondaryEmails || []).filter(
(secondaryEmail) => !updatedValues.secondaryEmails.find((val) => val.id && val.id === secondaryEmail.id)
);
const secondaryEmails = [
...updatedEmails.map((email) => ({ ...email, isDeleted: false })),
...deletedEmails.map((email) => ({ ...email, isDeleted: true })),
].map((secondaryEmail) => pick(secondaryEmail, ["id", "email", "isDeleted"]));
onSubmit({
...updatedValues,
secondaryEmails,
});
};
const {
formState: { isSubmitting, isDirty },
} = formMethods;
const isDisabled = isSubmitting || !isDirty;
return (
<Form form={formMethods} handleSubmit={onSubmit}>
<Form form={formMethods} handleSubmit={handleFormSubmit}>
<div className="border-subtle border-x px-4 pb-10 pt-8 sm:px-6">
<div className="flex items-center">
<Controller
@@ -508,12 +652,36 @@ const ProfileForm = ({
<TextField label={t("full_name")} {...formMethods.register("name")} />
</div>
<div className="mt-6">
<TextField
label={t("email")}
hint={t("change_email_hint")}
data-testId="profile-form-email"
{...formMethods.register("email")}
/>
<Label>{t("email")}</Label>
{secondaryEmailFields.map((field, index) => (
<CustomEmailTextField
key={field.itemId}
formMethods={formMethods}
formMethodFieldName={`secondaryEmails.${index}.email` as keyof FormValues}
errorMessage={get(formMethods.formState.errors, `secondaryEmails.${index}.email.message`)}
emailVerified={Boolean(field.emailVerified)}
emailPrimary={field.emailPrimary}
dataTestId={`profile-form-email-${index}`}
handleChangePrimary={() => {
const fields = secondaryEmailFields.map((secondaryField, cIndex) => ({
...secondaryField,
emailPrimary: cIndex === index,
}));
updateAllSecondaryEmailFields(fields);
}}
handleVerifyEmail={() => handleResendVerifyEmail(field.email)}
handleItemDelete={() => deleteSecondaryEmail(index)}
/>
))}
<div className="text-default mt-2 flex items-center text-sm">{t("change_email_hint")}</div>
<Button
color="minimal"
StartIcon={Plus}
className="mt-2"
onClick={() => handleAddSecondaryEmail()}
data-testid="add-secondary-email">
{t("add_email")}
</Button>
</div>
<div className="mt-6">
<Label>{t("about")}</Label>
@@ -535,7 +703,7 @@ const ProfileForm = ({
disabled={isDisabled}
color="primary"
type="submit"
data-testId="profile-submit-button">
data-testid="profile-submit-button">
{t("update")}
</Button>
</SectionBottomActions>
@@ -0,0 +1,9 @@
import DirectorySyncTeamView from "@calcom/features/ee/dsync/page/team-dsync-view";
import type { CalPageWrapper } from "@components/PageWrapper";
import PageWrapper from "@components/PageWrapper";
const Page = DirectorySyncTeamView as CalPageWrapper;
Page.PageWrapper = PageWrapper;
export default Page;
+42 -1
View File
@@ -1,22 +1,63 @@
"use client";
import Head from "next/head";
import { useRouter } from "next/navigation";
import { z } from "zod";
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RouterOutputs } from "@calcom/trpc/react";
import { WizardLayout } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
const querySchema = z.object({
returnTo: z.string().optional(),
slug: z.string().optional(),
});
const CreateNewTeamPage = () => {
const { t } = useLocale();
const params = useParamsWithFallback();
const parsedQuery = querySchema.safeParse(params);
const router = useRouter();
const telemetry = useTelemetry();
const isTeamBillingEnabledClient = !!process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY && HOSTED_CAL_FEATURES;
const flag = isTeamBillingEnabledClient
? {
telemetryEvent: telemetryEventTypes.team_checkout_session_created,
submitLabel: "checkout",
}
: {
telemetryEvent: telemetryEventTypes.team_created,
submitLabel: "continue",
};
const returnToParam =
(parsedQuery.success ? getSafeRedirectUrl(parsedQuery.data.returnTo) : "/teams") || "/teams";
const onSuccess = (data: RouterOutputs["viewer"]["teams"]["create"]) => {
telemetry.event(flag.telemetryEvent);
router.push(data.url);
};
return (
<>
<Head>
<title>{t("create_new_team")}</title>
<meta name="description" content={t("create_new_team_description")} />
</Head>
<CreateANewTeamForm />
<CreateANewTeamForm
slug={parsedQuery.success ? parsedQuery.data.slug : ""}
onCancel={() => router.push(returnToParam)}
submitLabel={flag.submitLabel}
onSuccess={onSuccess}
/>
</>
);
};
+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) => {
+4 -4
View File
@@ -62,7 +62,7 @@ function TeamPage({
return (
<div className="flex h-full min-h-[100dvh] items-center justify-center">
<UnpublishedEntity
{...(metadata?.isOrganization || team.parentId ? { orgSlug: slug } : { teamSlug: slug })}
{...(team?.isOrganization || team.parentId ? { orgSlug: slug } : { teamSlug: slug })}
name={teamName}
/>
</div>
@@ -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">
@@ -158,7 +158,7 @@ function TeamPage({
);
const profileImageSrc =
isValidOrgDomain || team.metadata?.isOrganization
isValidOrgDomain || team.isOrganization
? getOrgAvatarUrl({ slug: currentOrgDomain, logoUrl: team.logoUrl })
: getTeamAvatarUrl({ slug: team.slug, logoUrl: team.logoUrl, organizationId: team.parent?.id });
@@ -193,7 +193,7 @@ function TeamPage({
</>
)}
</div>
{metadata?.isOrganization ? (
{team.isOrganization ? (
<SubTeams />
) : (
<>
+3
View File
@@ -25,6 +25,7 @@ export const getMultipleDurationValue = (
if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration);
return defaultValue;
};
export default function Type({
slug,
user,
@@ -34,6 +35,7 @@ export default function Type({
isBrandingHidden,
eventData,
isInstantMeeting,
orgBannerUrl,
}: PageProps) {
const searchParams = useSearchParams();
@@ -66,6 +68,7 @@ export default function Type({
searchParams?.get("duration"),
eventData.length
)}
orgBannerUrl={orgBannerUrl}
/>
</main>
);
@@ -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);
});
});
});
@@ -2,6 +2,9 @@ import { expect } from "@playwright/test";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { MembershipRole } from "@calcom/prisma/enums";
import { moveUserToOrg } from "@lib/orgMigration";
import { test } from "./lib/fixtures";
@@ -110,4 +113,54 @@ test.describe("Change username on settings", () => {
await expect(page).toHaveURL(/.*checkout.stripe.com/);
});
test("User can't take a username that has been migrated to a different username in an organization", async ({
users,
orgs,
page,
}) => {
const existingUser =
await test.step("Migrate user to a different username in an organization", async () => {
const org = await orgs.create({
name: "TestOrg",
});
const existingUser = await users.create({
username: "john",
emailDomain: org.organizationSettings?.orgAutoAcceptEmail ?? "",
name: "John Outside Organization",
});
await moveUserToOrg({
user: existingUser,
targetOrg: {
// Changed username. After this there is no user with username equal to {existingUser.username}
username: `${existingUser.username}-org`,
id: org.id,
membership: {
role: MembershipRole.MEMBER,
accepted: true,
},
},
shouldMoveTeams: false,
});
return existingUser;
});
await test.step("Changing username for another user to the previous username of migrated user - shouldn't be allowed", async () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const previousUsername = existingUser.username!;
const user = await users.create();
await user.apiLogin();
await page.goto("/settings/my-account/profile");
const usernameInput = page.locator("[data-testid=username-input]");
await usernameInput.fill(previousUsername);
await page.waitForLoadState("networkidle");
await expect(page.locator("[data-testid=update-username-btn]").nth(0)).toBeHidden();
await expect(page.locator("[data-testid=update-username-btn]").nth(1)).toBeHidden();
});
});
});
+4
View File
@@ -107,6 +107,10 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
expect(formTitle).toBe(firstTitle);
expect(formSlug).toContain(firstSlug);
const test = await page.getByTestId("continue").click();
const toast = await page.waitForSelector('[data-testid="toast-success"]');
expect(toast).toBeTruthy();
});
test("edit first event", async ({ page }) => {
+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();
},
};
}
+4 -1
View File
@@ -48,8 +48,8 @@ export async function createOrgInDb({
data: {
name: name,
slug: slug,
isOrganization: true,
metadata: {
isOrganization: true,
...(requestedSlug
? {
requestedSlug,
@@ -57,5 +57,8 @@ export async function createOrgInDb({
: null),
},
},
include: {
organizationSettings: true,
},
});
}
@@ -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");
},
};
}
+36 -15
View File
@@ -120,18 +120,21 @@ const createTeamAndAddUser = async (
const slug = `${isOrg ? "org" : "team"}-${workerInfo.workerIndex}-${Date.now()}`;
const data: PrismaType.TeamCreateInput = {
name: `user-id-${user.id}'s ${isOrg ? "Org" : "Team"}`,
isOrganization: isOrg,
};
data.metadata = {
...(isUnpublished ? { requestedSlug: slug } : {}),
...(isOrg
? {
isOrganization: true,
isOrganizationVerified: !!isOrgVerified,
orgAutoAcceptEmail: user.email.split("@")[1],
isOrganizationConfigured: false,
}
: {}),
};
if (isOrg) {
data.organizationSettings = {
create: {
isOrganizationVerified: !!isOrgVerified,
orgAutoAcceptEmail: user.email.split("@")[1],
isOrganizationConfigured: false,
},
};
}
data.slug = !isUnpublished ? slug : undefined;
if (isOrg && hasSubteam) {
const team = await createTeamAndAddUser({ user }, workerInfo);
@@ -518,6 +521,7 @@ export const createUsersFixture = (
// Delete all users that were tracked by email(if they were created)
await prisma.user.deleteMany({ where: { email: { in: store.trackedEmails.map((e) => e.email) } } });
await prisma.team.deleteMany({ where: { id: { in: store.teams.map((org) => org.id) } } });
await prisma.secondaryEmail.deleteMany({ where: { userId: { in: ids } } });
store.users = [];
store.teams = [];
store.trackedEmails = [];
@@ -593,25 +597,40 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
},
};
})
.find((membership) => !membership.team?.metadata?.isOrganization);
.find((membership) => !membership.team.isOrganization);
if (!membership) {
throw new Error("No team found for user");
}
return membership;
},
getOrgMembership: async () => {
return prisma.membership.findFirstOrThrow({
const membership = await prisma.membership.findFirstOrThrow({
where: {
userId: user.id,
team: {
metadata: {
path: ["isOrganization"],
equals: true,
isOrganization: true,
},
},
include: {
team: {
include: {
children: true,
organizationSettings: true,
},
},
},
include: { team: { include: { children: true } } },
});
if (!membership) {
return membership;
}
return {
...membership,
team: {
...membership.team,
metadata: teamMetadataSchema.parse(membership.team.metadata),
},
};
},
getFirstEventAsOwner: async () =>
prisma.eventType.findFirstOrThrow({
@@ -669,6 +688,7 @@ type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
roleInOrganization?: MembershipRole;
schedule?: Schedule;
password?: string | null;
emailDomain?: string;
};
// creates the actual user in the db.
@@ -686,10 +706,11 @@ const createUser = (
? opts.username
: `${opts?.username || "user"}-${workerInfo.workerIndex}-${Date.now()}`;
const emailDomain = opts?.emailDomain || "example.com";
return {
username: uname,
name: opts?.name,
email: opts?.email ?? `${uname}@example.com`,
email: opts?.email ?? `${uname}@${emailDomain}`,
password: {
create: {
hash: hashPassword(uname),
+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);
},
});
@@ -351,7 +351,7 @@ test.describe("Bookings", () => {
}
const usernameInOrg = getOrgUsernameFromEmail(
invitedUserEmail,
org.metadata?.orgAutoAcceptEmail ?? null
org.organizationSettings?.orgAutoAcceptEmail ?? null
);
const usernameOutsideOrg = userOutsideOrganization.username;
@@ -2,6 +2,9 @@ import type { Browser, Page } from "@playwright/test";
import { expect } from "@playwright/test";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/client";
import { moveUserToOrg } from "@lib/orgMigration";
import { test } from "../lib/fixtures";
import { getInviteLink } from "../lib/testUtils";
@@ -16,7 +19,7 @@ test.afterEach(async ({ users, orgs }) => {
test.describe("Organization", () => {
test.describe("Email not matching orgAutoAcceptEmail", () => {
test("Org Invitation", async ({ browser, page, users, emails }) => {
test("nonexisting user invited to an organization", async ({ browser, page, users, emails }) => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
const { team: org } = await orgOwner.getOrgMembership();
await orgOwner.apiLogin();
@@ -83,7 +86,17 @@ test.describe("Organization", () => {
});
});
test("Team invitation", async ({ browser, page, users, emails }) => {
// This test is already covered by booking.e2e.ts where existing user is invited and his booking links are tested.
// We can re-test here when we want to test some more scenarios.
// eslint-disable-next-line @typescript-eslint/no-empty-function
test("existing user invited to an organization", () => {});
test("nonexisting user invited to a Team inside organization", async ({
browser,
page,
users,
emails,
}) => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, hasSubteam: true });
await orgOwner.apiLogin();
const { team } = await orgOwner.getFirstTeamMembership();
@@ -183,7 +196,7 @@ test.describe("Organization", () => {
});
test.describe("Email matching orgAutoAcceptEmail and a Verified Organization", () => {
test("Org Invitation", async ({ browser, page, users, emails }) => {
test("nonexisting user is invited to Org", async ({ browser, page, users, emails }) => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
const { team: org } = await orgOwner.getOrgMembership();
await orgOwner.apiLogin();
@@ -248,7 +261,52 @@ test.describe("Organization", () => {
});
});
test("Team Invitation", async ({ browser, page, users, emails }) => {
// Such a user has user.username changed directly in addition to having the new username in the profile.username
test("existing user migrated to an organization", async ({ users, page, emails }) => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
const { team: org } = await orgOwner.getOrgMembership();
await orgOwner.apiLogin();
const { existingUser } = await test.step("Invite an existing user to an organization", async () => {
const existingUser = await users.create({
username: "john",
emailDomain: org.organizationSettings?.orgAutoAcceptEmail ?? "",
name: "John Outside Organization",
});
await moveUserToOrg({
user: existingUser,
targetOrg: {
username: `${existingUser.username}-org`,
id: org.id,
membership: {
role: MembershipRole.MEMBER,
accepted: true,
},
},
shouldMoveTeams: false,
});
return { existingUser };
});
await test.step("Signing up with the previous username of the migrated user - shouldn't be allowed", async () => {
await page.goto("/signup");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await page.locator('input[name="username"]').fill(existingUser.username!);
await page
.locator('input[name="email"]')
.fill(`${existingUser.username}-differnet-email@example.com`);
await page.locator('input[name="password"]').fill("Password99!");
await page.waitForLoadState("networkidle");
await expect(page.locator('button[type="submit"]')).toBeDisabled();
});
});
test("nonexisting user is invited to a team inside organization", async ({
browser,
page,
users,
emails,
}) => {
const orgOwner = await users.create(undefined, {
hasTeam: true,
isOrg: true,
+243 -19
View File
@@ -1,8 +1,14 @@
import { expect } from "@playwright/test";
import type { Page } from "@playwright/test";
import type { createUsersFixture } from "playwright/fixtures/users";
import { WEBAPP_URL } from "@calcom/lib/constants";
import type { PrismaClient } from "@calcom/prisma";
import type { createEmailsFixture } from "./fixtures/emails";
import { test } from "./lib/fixtures";
import { getEmailsReceivedByUser } from "./lib/testUtils";
import { expectInvitationEmailToBeReceived } from "./team/expects";
test.describe.configure({ mode: "parallel" });
@@ -48,7 +54,7 @@ test.describe("Update Profile", () => {
await user.apiLogin();
await page.goto("/settings/my-account/profile");
const emailInput = page.getByTestId("profile-form-email");
const emailInput = page.getByTestId("profile-form-email-0");
await emailInput.fill(email);
@@ -58,7 +64,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,12 +90,12 @@ 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();
await page.goto("/settings/my-account/profile");
const emailInputUpdated = page.getByTestId("profile-form-email");
const emailInputUpdated = page.getByTestId("profile-form-email-0");
expect(await emailInputUpdated.inputValue()).toEqual(user.email);
});
@@ -108,7 +114,7 @@ test.describe("Update Profile", () => {
await user.apiLogin();
await page.goto("/settings/my-account/profile");
const emailInput = page.getByTestId("profile-form-email");
const emailInput = page.getByTestId("profile-form-email-0");
await emailInput.fill(email);
@@ -118,11 +124,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,17 +142,16 @@ test.describe("Update Profile", () => {
await page.goto(verifyUrl);
const successLocator = await page.waitForSelector('[data-testId="toast-success"]');
expect(await successLocator.textContent()).toContain(email);
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");
await page.goto("/settings/my-account/profile");
const emailInputUpdated = await page.getByTestId("profile-form-email");
const emailInputUpdated = await page.getByTestId("profile-form-email-0");
expect(await emailInputUpdated.inputValue()).toEqual(email);
});
test("Can update a users email (verification disabled)", async ({ page, users, prisma, features }) => {
const emailVerificationEnabled = features.get("email-verification");
// eslint-disable-next-line playwright/no-conditional-in-test, playwright/no-skipped-test
@@ -166,7 +167,7 @@ test.describe("Update Profile", () => {
await user.apiLogin();
await page.goto("/settings/my-account/profile");
const emailInput = page.getByTestId("profile-form-email");
const emailInput = page.getByTestId("profile-form-email-0");
await emailInput.fill(email);
@@ -176,12 +177,235 @@ test.describe("Update Profile", () => {
await page.getByTestId("profile-update-email-submit-button").click();
const toastLocator = await page.waitForSelector('[data-testId="toast-success"]');
expect(await page.getByTestId("toast-success").isVisible()).toBe(true);
expect(await toastLocator.isVisible()).toBe(true);
const emailInputUpdated = page.getByTestId("profile-form-email");
const emailInputUpdated = page.getByTestId("profile-form-email-0");
expect(await emailInputUpdated.inputValue()).toEqual(email);
});
const testEmailVerificationLink = async ({
page,
prisma,
emails,
secondaryEmail,
}: {
page: Page;
prisma: PrismaClient;
emails: ReturnType<typeof createEmailsFixture>;
secondaryEmail: string;
}) => {
await test.step("the user receives the correct invitation link", async () => {
await page.waitForLoadState("networkidle");
const verificationToken = await prisma.verificationToken.findFirst({
where: {
identifier: secondaryEmail,
},
});
const inviteLink = await expectInvitationEmailToBeReceived(
page,
emails,
secondaryEmail,
"Verify your email address",
"verify-email"
);
expect(inviteLink).toEqual(`${WEBAPP_URL}/api/auth/verify-email?token=${verificationToken?.token}`);
});
};
test("Can add a new email as a secondary email", async ({ page, users, prisma, emails }) => {
const user = await users.create({
name: "update-profile-user",
});
const [emailInfo, emailDomain] = user.email.split("@");
await user.apiLogin();
await page.goto("/settings/my-account/profile");
await page.getByTestId("add-secondary-email").click();
const secondaryEmailAddDialog = await page.waitForSelector('[data-testid="secondary-email-add-dialog"]');
expect(await secondaryEmailAddDialog.isVisible()).toBe(true);
const secondaryEmail = `${emailInfo}-secondary-email@${emailDomain}`;
const secondaryEmailInput = page.getByTestId("secondary-email-input");
await secondaryEmailInput.fill(secondaryEmail);
await page.getByTestId("add-secondary-email-button").click();
const secondaryEmailConfirmDialog = await page.waitForSelector(
'[data-testid="secondary-email-confirm-dialog"]'
);
expect(await secondaryEmailConfirmDialog.isVisible()).toBe(true);
const textContent = await secondaryEmailConfirmDialog.textContent();
expect(textContent).toContain(secondaryEmail);
await page.getByTestId("secondary-email-confirm-done-button").click();
expect(await secondaryEmailConfirmDialog.isVisible()).toBe(false);
await test.step("the user receives the correct invitation link", async () => {
await page.waitForLoadState("networkidle");
const verificationToken = await prisma.verificationToken.findFirst({
where: {
identifier: secondaryEmail,
},
});
const inviteLink = await expectInvitationEmailToBeReceived(
page,
emails,
secondaryEmail,
"Verify your email address",
"verify-email"
);
expect(inviteLink?.endsWith(`/api/auth/verify-email?token=${verificationToken?.token}`)).toEqual(true);
});
const primaryEmail = page.getByTestId("profile-form-email-0");
expect(await primaryEmail.inputValue()).toEqual(user.email);
const newlyAddedSecondaryEmail = page.getByTestId("profile-form-email-1");
expect(await newlyAddedSecondaryEmail.inputValue()).toEqual(secondaryEmail);
});
const createSecondaryEmail = async ({
page,
users,
}: {
page: Page;
users: ReturnType<typeof createUsersFixture>;
}) => {
const user = await users.create({
name: "update-profile-user",
});
const [emailInfo, emailDomain] = user.email.split("@");
const email = `${emailInfo}@${emailDomain}`;
await user.apiLogin();
await page.goto("/settings/my-account/profile");
await page.getByTestId("add-secondary-email").click();
const secondaryEmail = `${emailInfo}-secondary-email@${emailDomain}`;
const secondaryEmailInput = await page.getByTestId("secondary-email-input");
await secondaryEmailInput.fill(secondaryEmail);
await page.getByTestId("add-secondary-email-button").click();
await page.getByTestId("secondary-email-confirm-done-button").click();
return { user, email, secondaryEmail };
};
test("Newly added secondary email should show as Unverified", async ({ page, users }) => {
await createSecondaryEmail({ page, users });
expect(await page.getByTestId("profile-form-email-0-primary-badge").isVisible()).toEqual(true);
expect(await page.getByTestId("profile-form-email-0-unverified-badge").isVisible()).toEqual(false);
expect(await page.getByTestId("profile-form-email-1-primary-badge").isVisible()).toEqual(false);
expect(await page.getByTestId("profile-form-email-1-unverified-badge").isVisible()).toEqual(true);
});
test("Can verify the newly added secondary email", async ({ page, users, prisma }) => {
const { secondaryEmail } = await createSecondaryEmail({ page, users });
expect(await page.getByTestId("profile-form-email-1-primary-badge").isVisible()).toEqual(false);
expect(await page.getByTestId("profile-form-email-1-unverified-badge").isVisible()).toEqual(true);
// Instead of dealing with emails in e2e lets just get the token and navigate to it
const verificationToken = await prisma.verificationToken.findFirst({
where: {
identifier: secondaryEmail,
},
});
const params = new URLSearchParams({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
token: verificationToken!.token,
});
const verifyUrl = `${WEBAPP_URL}/api/auth/verify-email?${params.toString()}`;
await page.goto(verifyUrl);
expect(await page.getByTestId("profile-form-email-1-primary-badge").isVisible()).toEqual(false);
expect(await page.getByTestId("profile-form-email-1-unverified-badge").isVisible()).toEqual(false);
});
test("Can delete the newly added secondary email", async ({ page, users }) => {
await createSecondaryEmail({ page, users });
await page.getByTestId("secondary-email-action-group-button").nth(1).click();
await page.getByTestId("secondary-email-delete-button").click();
expect(await page.getByTestId("profile-form-email-1").isVisible()).toEqual(false);
});
test("Can make the newly added secondary email as the primary email and login", async ({
page,
users,
prisma,
}) => {
const { secondaryEmail } = await createSecondaryEmail({ page, users });
// Instead of dealing with emails in e2e lets just get the token and navigate to it
const verificationToken = await prisma.verificationToken.findFirst({
where: {
identifier: secondaryEmail,
},
});
const params = new URLSearchParams({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
token: verificationToken!.token,
});
const verifyUrl = `${WEBAPP_URL}/api/auth/verify-email?${params.toString()}`;
await page.goto(verifyUrl);
await page.getByTestId("secondary-email-action-group-button").nth(1).click();
await page.getByTestId("secondary-email-make-primary-button").click();
expect(await page.getByTestId("profile-form-email-1-primary-badge").isVisible()).toEqual(true);
expect(await page.getByTestId("profile-form-email-1-unverified-badge").isVisible()).toEqual(false);
});
test("Can resend verification link if the secondary email is unverified", async ({
page,
users,
prisma,
emails,
}) => {
const { secondaryEmail } = await createSecondaryEmail({ page, users });
// When a user is created a link is sent, we will delete it manually to make sure verification link works fine
await prisma.verificationToken.deleteMany({
where: {
identifier: secondaryEmail,
},
});
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail: secondaryEmail });
if (receivedEmails?.items?.[0]?.ID) {
await emails.deleteMessage(receivedEmails.items[0].ID);
}
expect(await page.getByTestId("profile-form-email-1-unverified-badge").isVisible()).toEqual(true);
await page.getByTestId("secondary-email-action-group-button").nth(1).click();
expect(await page.locator("button[data-testid=resend-verify-email-button]").isDisabled()).toEqual(false);
await page.getByTestId("resend-verify-email-button").click();
await testEmailVerificationLink({ page, prisma, emails, secondaryEmail });
const verificationToken = await prisma.verificationToken.findFirst({
where: {
identifier: secondaryEmail,
},
});
await page.goto(`${WEBAPP_URL}/api/auth/verify-email?token=${verificationToken?.token}`);
await page.getByTestId("secondary-email-action-group-button").nth(1).click();
expect(await page.locator("button[data-testid=resend-verify-email-button]").isVisible()).toEqual(false);
expect(await page.getByTestId("profile-form-email-1-unverified-badge").isVisible()).toEqual(false);
});
});
@@ -40,10 +40,8 @@ test.describe("UploadAvatar", async () => {
},
});
// todo: remove this; ideally the organization-avatar is updated the moment
// 'Settings updated succesfully' is saved.
await page.waitForLoadState("networkidle");
const avatar = page.getByTestId("profile-upload-avatar").locator("img");
// Wait for the avatar image with the http src to appear instead of the data uri
const avatar = page.getByTestId("profile-upload-avatar").locator("img[src^=http]");
const src = await avatar.getAttribute("src");
+40
View File
@@ -275,6 +275,46 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
await expect(page.locator('[data-testid="you-cannot-see-team-members"]')).toBeVisible();
await expect(page.locator('[data-testid="team-members-container"]')).toBeHidden();
});
test("Email Embeds slots are loading for team event types", async ({ page, users }) => {
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{ username: "pro-user", name: "pro-user" },
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.COLLECTIVE,
}
);
await owner.apiLogin();
const { team } = await owner.getFirstTeamMembership();
const {
title: teamEventTitle,
slug: teamEventSlug,
id: teamEventId,
} = await owner.getFirstTeamEvent(team.id);
await page.goto("/event-types");
await page.getByTestId(`event-type-options-${teamEventId}`).first().click();
await page.getByTestId("embed").click();
await page.getByTestId("email").click();
await page.getByTestId("incrementMonth").click();
await page.waitForLoadState("networkidle");
await expect(page.getByTestId("no-slots-available")).toBeHidden();
// Check Team Url
const availableTimesUrl = await page.getByTestId("see_all_available_times").getAttribute("href");
await expect(availableTimesUrl).toContain(`/team/${team.slug}/${teamEventSlug}`);
});
todo("Create a Round Robin with different leastRecentlyBooked hosts");
todo("Reschedule a Collective EventType booking");
File diff suppressed because it is too large Load Diff
+1
View File
@@ -47,6 +47,7 @@ test.describe("Workflow Tab - Event Type", () => {
await createWorkflow({ name: "A New Workflow", trigger: WorkflowTriggerEvents.NEW_EVENT });
await page.goto(`/${user.username}/${eventType.slug}`);
await page.click('[data-testid="incrementMonth"]');
await bookEventOnThisPage(page);
await assertWorkflowReminders(eventType.id, 1);
});
@@ -2061,6 +2061,7 @@
"workflow_automation": "أتمتة سير العمل",
"scheduling_for_your_team": "أتمتة سير العمل",
"no_members_found": "لم يُعثر على أعضاء",
"directory_sync": "مزامنة الدليل",
"event_setup_length_error": "إعداد الفعالية: يجب أن تكون المدة لدقيقة على الأقل.",
"availability_schedules": "جدولة التوافر",
"unauthorized": "غير مصرح به",
@@ -2061,6 +2061,7 @@
"workflow_automation": "Automatizace pracovních postupů",
"scheduling_for_your_team": "Automatizace pracovních postupů",
"no_members_found": "Nenalezeni žádní členové",
"directory_sync": "Synchronizace adresářů",
"event_setup_length_error": "Nastavení události: Doba trvání musí být alespoň 1 minuta.",
"availability_schedules": "Plány dostupnosti",
"unauthorized": "Neautorizováno",
@@ -2136,6 +2136,7 @@
"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",
"directory_sync": "Verzeichnissynchronisierung",
"event_setup_length_error": "Ereignis-Einrichtung: Die Dauer muss mindestens 1 Minute betragen.",
"availability_schedules": "Verfügbarkeitspläne",
"unauthorized": "Nicht authorisiert",
+38 -3
View File
@@ -131,6 +131,7 @@
"invitee_timezone": "Invitee Time Zone",
"time_left": "Time left",
"event_type": "Event Type",
"duplicate_event_type":"Duplicate Event Type",
"enter_meeting": "Enter Meeting",
"video_call_provider": "Video call provider",
"meeting_id": "Meeting ID",
@@ -251,6 +252,7 @@
"confirm_auth_change": "This will change the way you log in",
"confirm_auth_email_change": "Changing the email address will disconnect your current authentication method to log in to Cal.com. We will ask you to verify your new email address. Moving forward, you will be logged out and use your new email address to log in instead of your current authentication method after setting your password by following the instructions that will be sent to your mail.",
"reset_your_password": "Set your new password with the instructions sent to your email address.",
"org_banner_instructions":"Please upload image of {{width}} width and {{height}} height.",
"email_change": "Log back in with your new email address and password.",
"create_your_account": "Create your account",
"create_your_calcom_account": "Create your Cal.com account",
@@ -638,7 +640,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",
@@ -1473,8 +1475,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}}",
@@ -1599,7 +1603,8 @@
"attendee_email_variable": "Attendee email",
"attendee_email_info": "The person booking's email",
"kbar_search_placeholder": "Type a command or search...",
"invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.",
"invalid_credential": "It looks like permissions expired or were revoked for {{appName}}.",
"invalid_credential_action": "Reinstall app",
"reschedule_reason": "Reschedule reason",
"choose_common_schedule_team_event": "Choose a common schedule",
"choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule.",
@@ -2080,6 +2085,7 @@
"organizations": "Organizations",
"upload_cal_video_logo":"Upload Cal Video Logo",
"update_cal_video_logo":"Update Cal Video Logo",
"upload_banner": "Upload Banner",
"cal_video_logo_upload_instruction":"To ensure your logo is visible against Cal video's dark background, please upload a light-colored image in PNG or SVG format to maintain transparency.",
"org_admin_other_teams": "Other teams",
"org_admin_other_teams_description": "Here you can see teams inside your organization that you are not part of. You can add yourself to them if needed.",
@@ -2137,6 +2143,24 @@
"scheduling_for_your_team":"Workflow automation",
"scheduling_for_your_team_description":"Schedule for your team with collective and round-robin scheduling",
"no_members_found": "No members found",
"directory_sync" : "Directory Sync",
"directory_name": "Directory Name",
"directory_provider": "Directory Provider",
"directory_scim_url": "SCIM Base URL",
"directory_scim_token": "SCIM Bearer Token",
"directory_scim_url_copied": "SCIM Base URL copied",
"directory_scim_token_copied": "SCIM Bearer Token copied",
"directory_sync_info_description": "Your Identity Provider will ask for the following information to configure SCIM. Follow the instructions to finish the setup.",
"directory_sync_configure" : "Configure Directory Sync",
"directory_sync_configure_description" : "Choose an identity provider to configure directory for your team.",
"directory_sync_title": "Configure an identity provider to get started with SCIM.",
"directory_sync_created": "Directory sync connection created.",
"directory_sync_description": "Provision and de-provision users with your directory provider.",
"directory_sync_deleted": "Directory sync connection deleted.",
"directory_sync_delete_connection": "Delete Connection",
"directory_sync_delete_title": "Delete Directory Sync Connection",
"directory_sync_delete_description": "Are you sure you want to delete this directory sync connection?",
"directory_sync_delete_confirmation": "This action cannot be undone. This will permanently delete the directory sync connection.",
"event_setup_length_error":"Event Setup: The duration must be at least 1 minute.",
"availability_schedules":"Availability Schedules",
"unauthorized":"Unauthorized",
@@ -2263,5 +2287,16 @@
"field_identifiers_as_variables": "Use field identifiers as variables for your custom event redirect",
"field_identifiers_as_variables_with_example": "Use field identifiers as variables for your custom event redirect (e.g. {{variable}})",
"account_already_linked": "Account is already linked",
"team_will_be_under_org": "New teams will be under your organization",
"add_group_name": "Add group name",
"group_name": "Group Name",
"routers": "Routers",
"primary": "Primary",
"make_primary": "Make primary",
"add_email": "Add Email",
"add_email_description": "Add an email address to replace your primary or to use as an alternative email on your event types.",
"confirm_email": "Confirm your email",
"confirm_email_description": "We sent an email to <strong>{{email}}</strong>. Click the link in the email to verify this address.",
"send_event_details_to": "Send event details to",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -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",
@@ -2060,6 +2060,7 @@
"workflow_automation": "Automatización del flujo de trabajo",
"scheduling_for_your_team": "Automatización del flujo de trabajo",
"no_members_found": "No se encontraron miembros",
"directory_sync": "Directory Sync",
"event_setup_length_error": "Configuración del evento: la duración debe ser de al menos 1 minuto.",
"availability_schedules": "Horarios de disponibilidad",
"unauthorized": "Sin autorización",
@@ -251,6 +251,7 @@
"confirm_auth_change": "Cela changera la façon dont vous vous connectez",
"confirm_auth_email_change": "La modification de l'adresse e-mail déconnectera votre méthode d'authentification actuelle pour vous connecter à Cal.com. Nous vous demanderons de vérifier votre nouvelle adresse e-mail. Ensuite, vous serez déconnecté et devrez utiliser votre nouvelle adresse e-mail pour vous connecter après avoir défini votre mot de passe en suivant les instructions qui seront envoyées à votre adresse e-mail.",
"reset_your_password": "Définissez votre nouveau mot de passe avec les instructions envoyées à votre adresse e-mail.",
"org_banner_instructions": "Veuillez télécharger une image d'une largeur de {{width}} et d'une hauteur de {{height}}.",
"email_change": "Reconnectez-vous avec votre nouvelle adresse e-mail et votre nouveau mot de passe.",
"create_your_account": "Créez votre compte",
"create_your_calcom_account": "Créez votre compte Cal.com",
@@ -808,6 +809,7 @@
"label": "Libellé",
"placeholder": "Texte de substitution",
"display_add_to_calendar_organizer": "Afficher l'e-mail « Ajouter au calendrier » en tant qu'organisateur",
"reconnect_calendar_to_use": "Veuillez noter que vous devrez peut-être vous déconnecter puis reconnecter votre compte 'Ajouter au calendrier' pour utiliser cette fonctionnalité.",
"type": "Type",
"edit": "Modifier",
"add_input": "Ajouter un champ",
@@ -1474,6 +1476,8 @@
"copy_link_to_form": "Copier le lien vers le formulaire",
"theme": "Apparence",
"theme_applies_note": "Ceci s'applique uniquement à vos pages publiques de réservation.",
"app_theme": "Thème du tableau de bord",
"app_theme_applies_note": "Cela s'applique uniquement à votre tableau de bord connecté",
"theme_system": "Automatique",
"add_a_team": "Ajouter une équipe",
"add_webhook_description": "Recevez les données de rendez-vous en temps réel lorsque quelque chose se passe sur {{appName}}.",
@@ -1599,6 +1603,7 @@
"attendee_email_info": "Adresse e-mail du participant",
"kbar_search_placeholder": "Saisissez une commande ou une recherche...",
"invalid_credential": "Oh non ! On dirait que l’autorisation a expiré ou a été révoquée. Veuillez la réinstaller.",
"invalid_credential_action": "Réinstaller l'application",
"reschedule_reason": "Raison de la replanification",
"choose_common_schedule_team_event": "Choisissez un horaire commun",
"choose_common_schedule_team_event_description": "Activez cette option si vous souhaitez utiliser des disponibilités communes entre les hôtes. Si cette option est désactivée, chaque hôte recevra les réservations en fonction de ses disponibilités par défaut.",
@@ -2079,6 +2084,7 @@
"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",
"upload_banner": "Télécharger une bannière",
"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.",
@@ -2136,6 +2142,24 @@
"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é",
"directory_sync": "Synchronisation des dossiers",
"directory_name": "Nom du répertoire",
"directory_provider": "Fournisseur du répertoire",
"directory_scim_url": "URL de base SCIM",
"directory_scim_token": "Jeton du porteur SCIM",
"directory_scim_url_copied": "URL de base SCIM copiée",
"directory_scim_token_copied": "Le jeton SCIM Bearer a été copié",
"directory_sync_info_description": "Votre Fournisseur d'identité vous demandera les informations suivantes pour configurer SCIM. Suivez les instructions pour terminer l'installation.",
"directory_sync_configure": "Configurer la synchronisation du répertoire",
"directory_sync_configure_description": "Choisissez un fournisseur d'identité pour configurer le répertoire de votre équipe.",
"directory_sync_title": "Configurer un fournisseur d'identité pour commencer avec SCIM.",
"directory_sync_created": "Connexion de synchronisation du répertoire créée.",
"directory_sync_description": "Fourniture et dispense des utilisateurs avec votre fournisseur de répertoire.",
"directory_sync_deleted": "Connexion de synchronisation du répertoire supprimé.",
"directory_sync_delete_connection": "Supprimer la connexion",
"directory_sync_delete_title": "Supprimer la connexion de synchronisation de répertoire",
"directory_sync_delete_description": "Êtes-vous sûr de vouloir supprimer cette connexion de synchronisation de répertoire?",
"directory_sync_delete_confirmation": "Cette action ne peut pas être annulée. Cela supprimera définitivement la connexion de synchronisation du répertoire.",
"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é",
@@ -2262,5 +2286,13 @@
"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é",
"routers": "Routeurs",
"primary": "Primaire",
"make_primary": "Rendre primaire",
"add_email": "Ajouter un e-mail",
"add_email_description": "Ajoutez une adresse e-mail pour remplacer votre adresse principale ou pour utiliser comme adresse de courriel alternative sur vos types d'événements.",
"confirm_email": "Confirmez votre adresse e-mail",
"confirm_email_description": "Nous avons envoyé un e-mail à <strong>{{email}}</strong>. Cliquez sur le lien dans l'e-mail pour vérifier cette adresse.",
"send_event_details_to": "Envoyer les détails de l'événement à",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+31 -13
View File
@@ -1,6 +1,6 @@
{
"identity_provider": "ספק זהויות",
"trial_days_left": "נותרו לך $t(day, {\"count\": {{days}} }) לשימוש בגרסת הניסיון של רישוי PRO",
"trial_days_left": "יש לך עוד $t(day, {\"count\": {{days}} }) להתנסות בגרסת הפרו",
"day_one": "יום {{count}}",
"day_other": "{{count}} ימים",
"second_one": "שנייה {{count}}",
@@ -8,11 +8,11 @@
"upgrade_now": "לשדרג עכשיו",
"accept_invitation": "אישור הזמנה",
"calcom_explained": "{{appName}} מספקת תשתית ליצירת לוחות זמנים שתתאים לכולם.",
"calcom_explained_new_user": "סיים להגדיר את החשבון של {{appName}}! אתה רק מספר צעדים מפתרון כל בעיות התזמון שלך.",
"calcom_explained_new_user": "נא לסיים להגדיר את החשבון שלך ב־{{appName}}! בעוד מספר צעדים כל בעיות התזמון שלך ייפתרו.",
"have_any_questions": "יש לך שאלות? אנחנו כאן כדי לעזור.",
"reset_password_subject": "{{appName}}: הנחיות לאיפוס סיסמה",
"verify_email_subject": "{{appName}}: יש לאמת את החשבון",
"check_your_email": "בדוק/בדקי את הדוא״ל שלך",
"check_your_email": "נא לבדוק את הדוא״ל שלך",
"old_email_address": "כתובת דוא״ל ישנה",
"new_email_address": "כתובת דוא״ל חדשה",
"verify_email_page_body": "שלחנו הודעת דוא״ל לכתובת {{email}}. חשוב שתאמת/י את כתובת הדוא״ל שלך כדי להבטיח שתוכל/י לקבל הודעות מ-{{appName}} לכתובת הדוא״ל וללוח השנה שלך.",
@@ -29,20 +29,20 @@
"verify_email_by_code_email_body": "נא לאמת את כתובת הדוא״ל באמצעות הקוד שמתחת.",
"verify_email_email_link_text": "הנה הקישור, למקרה שאינך אוהב/ת ללחוץ על כפתורים:",
"email_verification_code": "הזנת קוד האימות",
"email_verification_code_placeholder": "יש להזין את קוד האימות שנשלח אליך בדוא\"ל",
"email_verification_code_placeholder": "נא למלא את קוד האימות שנשלח אליך בדוא״ל",
"incorrect_email_verification_code": "קוד האימות שגוי.",
"email_sent": "הודעת הדוא״ל נשלחה בהצלחה",
"email_not_sent": "אירעה שגיאה בעת שליחת הדוא\"ל",
"event_declined_subject": "נדחה: {{title}} ב- {{date}}",
"event_cancelled_subject": "בוטל: {{title}} ב-{{date}}",
"email_not_sent": "אירעה שגיאה בשליחת הדוא״ל",
"event_declined_subject": "נדחה: {{title}} ב־{{date}}",
"event_cancelled_subject": "בוטל: {{title}} ב־{{date}}",
"event_request_declined": "בקשת האירוע שלך נדחתה",
"event_request_declined_recurring": "בקשת האירוע החוזר שלך נדחתה",
"event_request_cancelled": "האירוע המתוכנן שלך בוטל",
"organizer": "מארגן",
"organizer": "ארגון",
"need_to_reschedule_or_cancel": "רוצה לקבוע מועד חדש או לבטל?",
"no_options_available": "אין אפשרויות זמינות",
"cancellation_reason": "הסיבה לביטול (לא חובה)",
"cancellation_reason_placeholder": "מדוע בחרת לבטל? (לא חובה)",
"cancellation_reason": "הסיבה לביטול (רשות)",
"cancellation_reason_placeholder": "מדוע בחרת לבטל?",
"rejection_reason": "הסיבה לדחייה",
"rejection_reason_title": "לדחות את בקשת ההזמנה?",
"rejection_reason_description": "בטוח שברצונך לדחות את ההזמנה? אנחנו ניידע את האדם שניסה להזמין. יש לך אפשרות להוסיף את הסיבה לדחייה למטה.",
@@ -50,7 +50,7 @@
"manage_this_event": "נהל את האירוע הזה",
"invite_team_member": "הזמן חבר צוות",
"invite_team_individual_segment": "הזמנת אדם אחד",
"invite_team_bulk_segment": "ייבוא בכמות גדולה",
"invite_team_bulk_segment": "ייבוא במרוכז",
"invite_team_notifcation_badge": "הזמנה",
"your_event_has_been_scheduled": "נקבע מועד לאירוע שלך",
"your_event_has_been_scheduled_recurring": "נקבע מועד לאירוע החוזר שלך",
@@ -86,7 +86,7 @@
"acquire_license": "כדי להסיר את התנאים האלה, ניתן לקנות רישיון מסחרי על ידי שליחת דוא\"ל",
"terms_summary": "סיכום התנאים",
"open_env": "יש לפתוח את ‎.env ולאשר את הרישיון שלנו",
"env_changed": "שיניתי את ה-‎.env שלי",
"env_changed": "שיניתי את ה־‎.env שלי",
"accept_license": "אישור הרישיון",
"still_waiting_for_approval": "אירוע עדיין ממתין לאישור",
"event_is_still_waiting": "בקשת האירוע עדיין בהמתנה: {{attendeeName}} - {{date}} - {{eventType}}",
@@ -104,7 +104,7 @@
"someone_requested_password_reset": "מישהו ביקש קישור כדי לשנות את הסיסמה שלך.",
"password_reset_email_sent": "אם כתובת הדוא\"ל הזו קיימת במערכת שלנו, אמור להישלח אליך דוא\"ל לאיפוס.",
"password_reset_instructions": "אם הבקשה לא היתה שלך, ניתן להתעלם מהודעת הדוא\"ל הזו בבטחה, והסיסמה שלך לא תשתנה.",
"event_awaiting_approval_subject": "ממתין לאישור: {{title}} ב- {{date}}",
"event_awaiting_approval_subject": "ממתין לאישור: {{title}} ב־{{date}}",
"event_still_awaiting_approval": "אירוע עדיין ממתין לאישורך",
"booking_submitted_subject": "ההזמנה נשלחה: {{title}} ב- {{date}}",
"download_recording_subject": "הורדת ההקלטה: {{title}} בתאריך {{date}}",
@@ -131,6 +131,7 @@
"invitee_timezone": "אזור הזמן של האדם שהוזמן",
"time_left": "זמן שנותר",
"event_type": "סוג האירוע",
"duplicate_event_type": "שכפול סוג אירוע",
"enter_meeting": "להצטרף לפגישה",
"video_call_provider": "ספק שיחות וידאו",
"meeting_id": "מזהה הפגישה",
@@ -251,6 +252,7 @@
"confirm_auth_change": "זה ישנה את דרך ההתחברות שלך",
"confirm_auth_email_change": "שינוי כתובת הדוא\"ל ינתק את שיטת האימות הנוכחית שמשמשת אותך לכניסה אל Cal.com. נבקש ממך לאמת את כתובת הדוא\"ל החדשה שלך. מעכשיו, המערכת תנתק אותך מהחשבון ויהיה עליך להשתמש בכתובת הדוא\"ל החדשה שלך כדי להתחבר, במקום בשיטת האימות הנוכחית שלך לאחר שתגדיר/י סיסמה לפי ההוראות שיישלחו אליך בדוא\"ל.",
"reset_your_password": "הגדר/י את הסיסמה החדשה שלך לפי ההוראות שנשלחו אל כתובת הדוא\"ל שלך.",
"org_banner_instructions": "נא להעלות תמונה ברוחב {{width}} וגובה {{height}}.",
"email_change": "התחבר/י שוב עם כתובת הדוא\"ל החדשה והסיסמה.",
"create_your_account": "צור את החשבון שלך",
"create_your_calcom_account": "יצירת החשבון שלך ב־Cal.com",
@@ -1475,6 +1477,8 @@
"copy_link_to_form": "להעתיק את הקישור לטופס",
"theme": "ערכת נושא",
"theme_applies_note": "חלה רק על דפים של הזמנות ציבוריות",
"app_theme": "ערכת עיצוב ללוח בקרה",
"app_theme_applies_note": "חל רק על לוח הבקרה שנכנסת אליו",
"theme_system": "ברירת המחדל של המערכת",
"add_a_team": "הוספת צוות",
"add_webhook_description": "לקבל נתוני פגישה בזמן אמת כשמשהו קורה ב-{{appName}}",
@@ -1600,6 +1604,7 @@
"attendee_email_info": "כתובת הדוא\"ל של האדם שביצע את ההזמנה",
"kbar_search_placeholder": "הקלד/י פקודה או חפש/י...",
"invalid_credential": "נראה שתוקף ההרשאה פג או שההרשאה בוטלה. יש להתקין מחדש שוב.",
"invalid_credential_action": "להתקין את היישום מחדש",
"reschedule_reason": "הסיבה לקביעת מועד חדש",
"choose_common_schedule_team_event": "בחירת לוח זמנים משותף",
"choose_common_schedule_team_event_description": "יש להפעיל אפשרות זו אם רוצים להשתמש בלוח זמנים משותף בין המארחים. אם האפשרות הזו מבוטלת, כל מארח יוזמן לפי לוח הזמנים המשמש כברירת המחדל שלו.",
@@ -2080,6 +2085,7 @@
"organizations": "ארגונים",
"upload_cal_video_logo": "העלאת סרטון לוגו ל־Cal",
"update_cal_video_logo": "עדכון סרטון לוגו ל־Cal",
"upload_banner": "העלאת כרזה",
"cal_video_logo_upload_instruction": "כדי לוודא שהלוגו שלך גלוי כנגד הרקע הכהה של הסרטון של Cal, נא להעלות תמונה בצבעים בהירים מהסוגים PNG או SVG כדי שהחלקים המתאימים יישארו שקופים.",
"org_admin_other_teams": "צוותים אחרים",
"org_admin_other_teams_description": "כאן תוכל/י לראות צוותים בארגון שאינך שייך/ת אליהם. יש לך אפשרות להוסיף את עצמך, במקרה הצורך.",
@@ -2137,6 +2143,10 @@
"scheduling_for_your_team": "אוטומצית תהליך עבודה",
"scheduling_for_your_team_description": "עריכת תזמון לצוות שלך באופן שיתופי ולפי התור",
"no_members_found": "לא נמצא אף חבר",
"directory_sync": "סנכרון ספריות",
"directory_name": "שם הספרייה",
"directory_provider": "ספק ספרייה",
"directory_sync_configure": "הגדרת סנכרון ספרייה",
"event_setup_length_error": "הגדרת אירוע: משך הזמן חייב להיות לפחות דקה אחת.",
"availability_schedules": "לוחות זמנים לזמינוּת",
"unauthorized": "אין הרשאה",
@@ -2263,5 +2273,13 @@
"field_identifiers_as_variables": "להשתמש במזהי השדות כמשתנים להפניות האירועים בהתאמה אישית",
"field_identifiers_as_variables_with_example": "להשתמש במזהי השדות כמשתנים להפניות האירועים בהתאמה אישית (למשל: {{variable}})",
"account_already_linked": "החשבון כבר מקושר",
"routers": "נתבים",
"primary": "עיקרי",
"make_primary": "הפיכה לעיקרי",
"add_email": "הוספת כתובת דוא״ל",
"add_email_description": "אפשר להוסיף כתובת דוא״ל כדי להחליף את הכתובת העיקרית שלך או ככתובת חלופית לסוגי האירועים שלך.",
"confirm_email": "אישור כתובת הדוא״ל שלך",
"confirm_email_description": "שלחנו הודעה בדוא״ל אל <strong>{{email}}</strong>. נא ללחוץ על הקישור בהודעה כדי לאמת את הכתובת הזאת.",
"send_event_details_to": "שליחת פרטי האירוע אל",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+185 -2
View File
@@ -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",
@@ -6,20 +7,77 @@
"second_other": "{{count}} másodperc",
"upgrade_now": "Frissíts most",
"accept_invitation": "Meghívó elfogadása",
"calcom_explained": "{{appName}} időpont-egyeztető infrastruktúrát biztosít mindenki számára.",
"calcom_explained_new_user": "Fejezd be a {{appName}} fiókod beállítását! Már csak pár lépésre vagy attól, hogy megoldd minden időpont-egyeztetési problémád.",
"have_any_questions": "Van kérdésed? Itt vagyunk hogy segítsünk.",
"reset_password_subject": "{{appName}}: Jelszóváltoztatási lépések",
"verify_email_subject": "{{appName}}: Igazold a fiókod",
"check_your_email": "Ellenőrizd az email fiókod",
"old_email_address": "Régi Email",
"new_email_address": "Új Email",
"verify_email_page_body": "Küldtünk egy emailt ide: {{email}}. Fontos, hogy igazold vissza az email címed, hogy {{appName}} biztosíthassa Neked a legjobb email és naptár funkciókat.",
"verify_email_banner_body": "Igazold vissza az email címed, hogy biztosíthassuk Neked a legjobb email és naptár funkciókat",
"verify_email_email_header": "Igazold vissza az email címed",
"verify_email_email_button": "Email igazolása",
"verify_email_change_success_toast": "Emailed módosítva a következőre: {{email}}",
"verify_email_change_failure_toast": "Sikertelen email módosítás.",
"change_of_email": "Igazold az új {{appName}} email címed",
"change_of_email_toast": "Küldtünk egy visszaigazoló linket ide: {{email}}. Módosítjuk az email címed amint a linkre kattintottál.",
"verify_email_email_body": "Kérlek igazold az email címed az alábbi gombra kattintással.",
"verify_email_by_code_email_body": "Kérlek igazold az email címed az alábbi kód használatával.",
"email_sent": "Email sikeresen elküldve",
"event_declined_subject": "Visszautasítva: {{title}}, {{date}}",
"event_cancelled_subject": "Lemondva: {{title}} ekkor: {{date}}",
"event_request_declined": "Az esemény kérésed vissza lett utasítva",
"event_request_declined_recurring": "Az ismétlődő esemény kérésed vissza lett utasítva",
"event_request_cancelled": "A lefoglalt időpontod vissza lett mondva",
"organizer": "Szervező",
"need_to_reschedule_or_cancel": "Átütemeznéd vagy lemondanád?",
"cancellation_reason": "Lemondás oka (opcionális)",
"cancellation_reason_placeholder": "Mi az oka, hogy le kell mondanod az időpontot?",
"rejection_reason": "Lemondás oka",
"rejection_reason_title": "Visszamondod a foglalási kérelmet?",
"rejection_reason_description": "Biztosan vissza szeretnéd utasítani a foglalást? Értesítjük azt aki próbált foglalni. Lejjebb megadhatsz egy okot.",
"rejection_confirmation": "Foglalás visszautasítása",
"your_event_has_been_scheduled": "Az időpontod le lett foglalva",
"your_event_has_been_scheduled_recurring": "Az ismétlődő időpontod le lett foglalva",
"refund_failed_subject": "Sikertelen visszatérítés: {{name}} - {{date}} - {{eventType}}",
"refund_failed": "Sikertelen visszatérítés ezért: {{eventType}}, {{userName}}, {{date}}.",
"a_refund_failed": "Sikertelen visszatérítés",
"awaiting_payment_subject": "Fizetésre vár: {{title}}, {{date}}",
"already_signed_up_for_this_booking_error": "Már jelentkeztél erre a foglalásra.",
"hosts_unavailable_for_booking": "Valaki a szervezők közül nem elérhető foglalásra.",
"help": "Segítség",
"price": "Ár",
"paid": "Fizetett",
"refunded": "Visszatérítve",
"missing_card_fields": "Hiányzó kártya adatok",
"pay_now": "Fizetés most",
"accept_license": "Licensz elfogadása",
"event_is_still_waiting": "Még mindig függőben lévő kérés: {{attendeeName}} - {{date}} - {{eventType}}",
"confirmed_event_type_subject": "Megerősítve: {{eventType}}, {{name}} ekkor: {{date}}",
"new_event_request": "Új időpontkérés: {{attendeeName}} - {{date}} - {{eventType}}",
"check_bookings_page_to_confirm_or_reject": "Nézd meg a foglalási oldalad hogy jóváhagyd vagy visszautasítsd a foglalást.",
"event_awaiting_approval_subject": "Jóváhagyásra vár: {{title}}, {{date}}",
"booking_submitted_subject": "Foglalás elküldve: {{title}}, {{date}}",
"download_recording_subject": "Felvétel letöltése: {{title}}, {{date}}",
"your_meeting_has_been_booked": "A találkozód le lett foglalva",
"event_type_has_been_rescheduled_on_time_date": "{{title}} át lett ütemezve ekkorra: {{date}}.",
"event_has_been_rescheduled": "Frissítve - Az időpontod át lett ütemezve",
"request_reschedule_subtitle": "{{organizer}} lemondta a foglalást és kérte, hogy válassz egy másik időpontot.",
"request_reschedule_title_organizer": "{{attendee}} időpontmódosítást kér",
"request_reschedule_subtitle_organizer": "Lemondtad a foglalást és {{attendee}} választhat új időpontot veled.",
"rescheduled_event_type_subject": "Átütemezési kérés kikülve: {{eventType}}, {{name}}, {{date}}",
"requested_to_reschedule_subject_attendee": "Átütmezés szükséges: Kérlek foglalj új időpontot a következőre: {{eventType}}, {{name}}",
"hi_user_name": "Szia {{name}}",
"new_event_subject": "Új esemény: {{attendeeName}} - {{date}} - {{eventType}}",
"notes": "Jegyzetek",
"need_to_make_a_change": "Változtatnál valamit?",
"new_event_scheduled_recurring": "Egy új ismétlődő időpont le lett foglalva.",
"meeting_id": "Megbeszélés azonosítója",
"meeting_password": "Megbeszélés jelszava",
"meeting_url": "Megbeszélés URL címe",
"rejected_event_type_with_organizer": "Visszautasítva: {{eventType}}, {{organizer}}, {{date}}",
"hi": "Szia",
"hey_there": "Üdv itt,",
"confirm_delete_webhook": "Igen, webhook törlése",
@@ -53,6 +111,7 @@
"documentation": "Dokumentáció",
"blog": "Blog",
"404_page_not_found": "404: Az oldal nem található.",
"booker_event_not_found": "Nem találjuk az eseményt amire szeretnél foglalni.",
"15min_meeting": "15 perces megbeszélés",
"30min_meeting": "30 perces megbeszélés",
"secret": "Titok",
@@ -67,6 +126,8 @@
"done": "Kész",
"all": "Minden alkalmazás",
"finish": "Befelyezés",
"continue_without_calendar": "Folyatatás naptár nélkül",
"continue_with": "Folyatatás {{appName}}-val",
"current_time": "Jelenlegi idő",
"welcome": "Üdvözöljük",
"connect_caldav": "Csatlakozás a CalDav szerverhez",
@@ -79,9 +140,30 @@
"where": "Hol",
"add_to_calendar": "Hozzáadás naptárhoz",
"other": "Egyéb",
"email_sign_in_subject": "Bejelentkezési link a következőhöz: {{appName}}",
"emailed_you_and_attendees": "Mindenkinek küldtünk egy emailt a naptár meghívóval és a részletekkel.",
"emailed_you_and_attendees_recurring": "Mindenkinek küldtünk emailben egy naptár meghívót a részletekkel az első ismétlődő eseményről.",
"emailed_you_and_any_other_attendees": "Küldtünk egy email mindenkinek ezekről az információkról.",
"user_needs_to_confirm_or_reject_booking": "{{user}} még mindig vissza kell igazolni vagy elutasítania a foglalást.",
"user_needs_to_confirm_or_reject_booking_recurring": "{{user}} résztvevőnek még mindig vissza kell igazolnia vagy elutasítania a foglalást az ismétlődő találkozóra.",
"meeting_is_scheduled": "Találkozó sikeresen lefoglalva",
"meeting_is_scheduled_recurring": "Az ismétlődő időpontod le lett foglalva",
"booking_submitted": "A foglalásod be lett küldve",
"booking_confirmed": "A foglalásod vissza lett igazolva",
"try_again": "Próbáld újra",
"login": "Bejelentkezés",
"success": "Siker",
"back_to_bookings": "Vissza a foglalásokhoz",
"cancelled": "Lemondva",
"cancellation_successful": "Sikeres lemondás",
"really_cancel_booking": "Biztosan lemondod a foglalásod?",
"cannot_cancel_booking": "Ezt a foglalást nem tudod lemondani",
"reschedule_instead": "Helyette át is ütemezheted.",
"cancelling_event_recurring": "Ez az esemény része egy ismétlődő eseménynek.",
"cancelling_all_recurring": "Ez az összes hátralévő esemény az ismétlőd eseményben.",
"booking_already_cancelled": "Ez a foglalás már le lett mondva",
"event_between_users": "{{eventName}} {{host}} és {{attendeeName}} közt",
"cancelled_bookings": "A lemondott foglalásaid itt fognak megjelenni.",
"and": "és",
"light": "Világos",
"dark": "Sötét",
@@ -89,6 +171,13 @@
"email_placeholder": "pista@pelda.hu",
"full_name": "Teljes név",
"create_webhook": "Webhook létrehozása",
"booking_cancelled": "Foglalás lemondva",
"booking_rescheduled": "Foglalás átütemezve",
"booking_created": "Foglalás létrehozva",
"booking_rejected": "Foglalás visszautasítva",
"booking_requested": "Foglalás kérvényezve",
"booking_payment_initiated": "Foglalási fizetés elindítva",
"booking_paid": "Foglalás kifizetve",
"new_webhook": "Új Webhook",
"loading": "Betöltés...",
"deleting": "Törlés...",
@@ -121,13 +210,24 @@
"friday": "Péntek",
"saturday": "Szombat",
"sunday": "Vasárnap",
"additional_guests": "További résztvevők",
"your_name": "Neved",
"email_address": "Email cím",
"location": "Helyszín",
"in_person_attendee_address": "Személyesen (Résztvevő címén)",
"yes": "Igen",
"no": "Nem",
"additional_notes": "Egyéb jegyzetek",
"booking_fail": "Találkozó nem foglalható.",
"reschedule_fail": "Nem sikerült időpontot módisítani.",
"share_additional_notes": "Kérlek ossz meg velünk bármit, ami segíthet felkészülni a találkozóra.",
"booking_reschedule_confirmation": "Módosítsd {{eventTypeTitle}} időpontod nála: {{profileName}}",
"in_person_meeting": "Személyes találkozó",
"in_person": "Személyesen (Szervező címén)",
"phone_number": "Telefonszám",
"organizer_phone_number": "Szervező telefonszáma",
"reschedule": "Átütmezés",
"reschedule_this": "Inkább időpontmódosítás",
"or": "VAGY",
"go_back": "Vissza",
"email_or_username": "Email vagy felhasználónév",
@@ -136,8 +236,11 @@
"danger_zone": "Veszélyzóna",
"back": "Vissza",
"cancel": "Mégse",
"cancel_all_remaining": "Minden hátralévő lemondása",
"apply": "Alkalmazás",
"cancel_event": "Időpont lemondása",
"continue": "Folytatás",
"confirm": "Rendben",
"submit": "Beküldés",
"delete": "Törlés",
"update": "Frissítés",
@@ -145,7 +248,9 @@
"share": "Megosztás",
"title": "Cím",
"description": "Leírás",
"apps_status": "Alkalmazások állapota",
"duration": "Hossz",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"minutes": "Perc",
"url": "URL",
"hidden": "Rejtett",
@@ -153,15 +258,21 @@
"your_email": "Email címed",
"language": "Nyelv",
"timezone": "Időzóna",
"every_for_freq": "Minden {{freq}}",
"repeats_every": "Ismétlődik minden",
"occurrence_other": "alkalomig",
"weekly_one": "hét",
"weekly_other": "hét",
"monthly_one": "hónap",
"monthly_other": "hónap",
"yearly_one": "év",
"yearly_other": "év",
"plus_more": "{{count}} további alkalom",
"name": "Név",
"logo": "Logo",
"logo": "Logó",
"error": "Hiba",
"meeting_with_user": "{Event type title} {Organiser} és {Scheduler} közt",
"display_add_to_calendar_organizer": "Jelenítse meg a 'Hozzáadás naptárhoz' emailt mint szervező",
"type": "Típus",
"edit": "Szerkesztés",
"starting": "Kezdés",
@@ -187,13 +298,26 @@
"add": "Hozzáadás",
"error_404": "404-es hiba",
"default": "Alapértelmezett",
"12_hour": "12 óra",
"24_hour": "24 óra",
"12_hour_short": "12ó",
"24_hour_short": "0-24",
"api_keys": "API kulcsok",
"create_api_key": "API kulcs létrehozása",
"edit_api_key": "API kulcs szerkesztése",
"create": "Létrehozás",
"copy": "Másolás",
"request_reschedule_booking": "Kérés a foglalás átütemezésére",
"reason_for_reschedule": "Ok az időpontmódosításra",
"book_a_new_time": "Foglalj új időpontot",
"reschedule_request_sent": "Időpontmódosítás kérelem elküldve",
"reschedule_modal_description": "Ez lemondja a lefoglalt időpontot, értesíti a foglalót és megkéri, hogy válasszon új időpontot.",
"reason_for_reschedule_request": "Időpontmódosítási kérelem oka",
"send_reschedule_request": "Időpontmódosítást kérek ",
"reschedule_booking": "Időpontmódosítás",
"former_time": "Korábbi időpont",
"search": "Keresés",
"location_changed_event_type_subject": "Helyszín módosult: {{eventType}}, {{name}}, {{date}}",
"copy_code": "Kód másolása",
"code_copied": "Kód kimásolva!",
"generate_api_key": "API kulcs generálása",
@@ -202,12 +326,71 @@
"search_giphy": "Keresls Giphy-n",
"feedback": "Visszajelzés",
"or_lowercase": "vagy",
"nevermind": "Mégsem",
"reschedule_optional": "Időpontmódosítás oka (opcionális)",
"reschedule_placeholder": "Add mások tudtára miért van szükséged időpontmódosításra",
"event_cancelled": "Ez az esemény le lett mondva",
"emailed_information_about_cancelled_event": "Mindenkinek küldtünk emailt, hogy tudjanak róla.",
"event_cancelled_trigger": "amikor az esemény le lett mondva",
"reschedule_event_trigger": "amikor az időpont módosítva lett",
"hour_timeUnit": "Óra",
"minute_timeUnit": "Perc",
"remove_app": "Alkalmazás eltávolítása",
"yes_remove_app": "Igen, távolítsd el az alkalmazást",
"web_conference": "Online konferencia",
"rescheduled": "Átütemezve",
"reminder_email": "Emlékeztető: {{eventType}}, {{name}}, {{date}}",
"minute_one": "{{count}} perc",
"minute_other": "{{count}} perc",
"hour_one": "{{count}} óra",
"hour_other": "{{count}} óra",
"new_seat_subject": "Új résztvevő {{name}} itt: {{eventType}}, {{date}}",
"organizer_name_variable": "Szervező név",
"cancelled_bookings_cannot_be_rescheduled": "Lemondott foglalásokat nem lehet átütemezni",
"email_subject": "Email tárgy",
"event_type_not_found": "Eseménytípus nem található",
"location_variable": "Helyszín",
"additional_notes_variable": "Egyéb jegyzetek",
"timezone_variable": "Időzóna"
"additional_notes_info": "A találkozó egyéb megjegyzései",
"organizer_name_info": "Szervező neve",
"require_additional_notes": "Kötelező kitölteni az egyéb megjegyzéseket",
"require_additional_notes_description": "Kötelező kitölteni az egyéb megjegyzéeseket a foglaláshoz",
"invalid_credential": "Úgy tűnik a jogosultságok lejártak vagy vissza lettek vonva a következőhöz: {{appName}}.",
"invalid_credential_action": "Alkalmazás újratelepítése",
"reschedule_reason": "Időpontmódosítás oka",
"reason": "Ok",
"email_subject_slug_replacement": "Egy csapat adminisztrátor kicserélte az eseményed /{{slug}}",
"app_disabled_subject": "{{appName}} le lett tiltva",
"organizer_timezone": "Szervező időzónája",
"event_no_longer_attending_subject": "Már nem vesz részt itt: {{title}}, {{date}}",
"attendee_no_longer_attending_subject": "Egy résztvevő nem vesz részt itt: {{title}}, {{date}}",
"attendee_no_longer_attending_subtitle": "{{name}} le lett mondva. Ez azt jelenti, hogy egy hely felszabadult erre az időpontra",
"booking_with_payment_cancelled": "Fizetés ezért az eseményért már nem lehetséges",
"booking_with_payment_cancelled_already_paid": "A visszatérítés ehhez a foglaláshoz úton van.",
"booking_with_payment_cancelled_refunded": "Ez a kifizetett foglalás már vissza lett térítve.",
"timezone_variable": "Időzóna",
"cancel_url_variable": "Lemondás URL",
"cancel_url_info": "URL a foglalás lemondásához",
"reschedule_url_variable": "Időpontmódosítási URL",
"reschedule_url_info": "Az URL az időpontmódosításhoz",
"events_cancelled": "Lemondott Események",
"events_rescheduled": "Átütemezett események",
"no_show_fee_charged_email_subject": "Meg nem jelenési díj {{amount, currency}} levonva a következőért: {{title}}, {{date}}",
"complete_your_booking_subject": "Fejezd be a foglalásod: {{title}}, {{date}}",
"google_new_spam_policy": "Google új levélszemét szabályzata megakadályozhatja, hogy megérkezzenek az email és naptár értesítések erről a foglalásról.",
"resolve": "Megoldás",
"what_is_this_meeting_about": "Miről szól ez a találkozó?",
"admin_org_notification_email_subject": "Új szervezet létrehozva: függőben",
"overlay_my_calendar": "Összenézem a naptárammal",
"overlay_my_calendar_toc": "A naptár csatlakoztatással Ön elfogadja az adatkezelési- és a felhasználási szabályzatunkat. Bármikor visszavonhatja a hozzáférést.",
"view_overlay_calendar_events": "Nézd meg a naptáreseményeidet, hogy megelőzd az ütköző foglalásokat.",
"need_help": "Segítségre van szükséged?",
"booking_redirect_email_subject": "Foglalás átirányítás értesítő",
"primary": "Elsődleges",
"make_primary": "Legyen elsődleges",
"add_email": "Email hozzáadása",
"add_email_description": "Adj hozzá egy email címet, hogy lecseréld az elsődleges címed, vagy hogy használhass egy alternatív emailt az esemény típusoknél.",
"confirm_email": "Igazold az email címed",
"confirm_email_description": "Küldtünk egy emailt a <strong>{{email}}</strong> címre. Kattints a linkre a levélben, hogy igazold az email címed.",
"send_event_details_to": "Esemény részleteinek küldése a következőnek"
}
@@ -2136,6 +2136,7 @@
"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",
"directory_sync": "Directory Sync",
"event_setup_length_error": "Impostazione evento: la durata deve essere di almeno 1 minuto.",
"availability_schedules": "Calendario disponibilità",
"unauthorized": "Non autorizzato",
@@ -2061,6 +2061,7 @@
"workflow_automation": "ワークフローの自動化",
"scheduling_for_your_team": "ワークフローの自動化",
"no_members_found": "メンバーが見つかりません",
"directory_sync": "Directory Sync",
"event_setup_length_error": "イベント設定:時間は 1 分以上でなくてはいけません。",
"availability_schedules": "空き状況一覧",
"unauthorized": "権限がありません",
@@ -2061,6 +2061,7 @@
"workflow_automation": "워크플로 자동화",
"scheduling_for_your_team": "워크플로 자동화",
"no_members_found": "구성원 없음",
"directory_sync": "디렉토리 동기화",
"event_setup_length_error": "이벤트 설정: 지속 시간은 1분 이상이어야 합니다.",
"availability_schedules": "사용 가능한 일정",
"unauthorized": "미승인",
@@ -2061,6 +2061,7 @@
"workflow_automation": "Automatisering van de werkstroom",
"scheduling_for_your_team": "Automatisering van de werkstroom",
"no_members_found": "Geen leden gevonden",
"directory_sync": "Mapsynchronisatie",
"event_setup_length_error": "Gebeurtenisconfiguratie: de duur moet minimaal 1 minuut zijn.",
"availability_schedules": "Beschikbaarheidsschema's",
"unauthorized": "Ongeautoriseerd",
@@ -2061,6 +2061,7 @@
"workflow_automation": "Automatyzacja przepływu pracy",
"scheduling_for_your_team": "Automatyzacja przepływu pracy",
"no_members_found": "Nie znaleziono członków",
"directory_sync": "Synchronizacja katalogu",
"event_setup_length_error": "Konfiguracja wydarzenia: czas trwania musi wynosić co najmniej minutę.",
"availability_schedules": "Harmonogramy dostępności",
"unauthorized": "Brak autoryzacji",
@@ -2096,6 +2096,7 @@
"workflow_automation": "Automação de fluxo de trabalho",
"scheduling_for_your_team": "Automação de fluxo de trabalho",
"no_members_found": "Nenhum membro encontrado",
"directory_sync": "Sincronização de diretório",
"event_setup_length_error": "Configuração do evento: deve ter pelo menos 1 minuto de duração.",
"availability_schedules": "Agendamentos de disponibilidade",
"unauthorized": "Não autorizado",
@@ -2136,6 +2136,7 @@
"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",
"directory_sync": "Sincronização do diretório",
"event_setup_length_error": "Configuração do Evento: a duração deve ser de, pelo menos, 1 minuto.",
"availability_schedules": "Horários de Disponibilidade",
"unauthorized": "Não autorizado",
@@ -2061,6 +2061,7 @@
"workflow_automation": "Automatizarea fluxului de lucru",
"scheduling_for_your_team": "Automatizarea fluxului de lucru",
"no_members_found": "Nu s-a găsit niciun membru",
"directory_sync": "Sincronizare director",
"event_setup_length_error": "Configurare eveniment: Durata trebuie să fie de cel puțin 1 minut.",
"availability_schedules": "Programe de disponibilitate",
"unauthorized": "Neautorizat",
@@ -2062,6 +2062,7 @@
"workflow_automation": "Автоматизация при помощи рабочих процессов",
"scheduling_for_your_team": "Автоматизация при помощи рабочих процессов",
"no_members_found": "Участники не найдены",
"directory_sync": "Синхронизация каталогов",
"event_setup_length_error": "Настройка события: продолжительность должна быть не менее 1 минуты.",
"availability_schedules": "График с информацией о доступности",
"unauthorized": "Неавторизован(а)",
@@ -2061,6 +2061,7 @@
"workflow_automation": "Automatizacija radnog toka",
"scheduling_for_your_team": "Automatizacija radnog toka",
"no_members_found": "Članovi nisu pronađeni",
"directory_sync": "Sinhronizacija za Directory",
"event_setup_length_error": "Podešavanje događaja: Trajanje mora da bude barem 1 minut.",
"availability_schedules": "Raspored dostupnosti",
"unauthorized": "Neovlašćeno",
@@ -2061,6 +2061,7 @@
"workflow_automation": "Automatisering av arbetsflöde",
"scheduling_for_your_team": "Automatisering av arbetsflöde",
"no_members_found": "Inga medlemmar hittades",
"directory_sync": "Katalogsynkronisering",
"event_setup_length_error": "Konfigurering av evenemang: Längden måste vara minst 1 minut.",
"availability_schedules": "Scheman för tillgänglighet",
"unauthorized": "Obehörig",
@@ -2061,6 +2061,7 @@
"workflow_automation": "İş akışı otomasyonu",
"scheduling_for_your_team": "İş akışı otomasyonu",
"no_members_found": "Üye bulunamadı",
"directory_sync": "Dizin Senkronizasyonu",
"event_setup_length_error": "Etkinlik Kurulumu: Süre en az 1 dakika olmalıdır.",
"availability_schedules": "Müsaitlik Planları",
"unauthorized": "Yetkisiz",
@@ -13,10 +13,17 @@
"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": "Підтвердьте свою електронну адресу",
"verify_email_email_button": "Підтвердити ел. адресу",
"verify_email_change_description": "Ви нещодавно просили змінити адресу електронної пошти, яку ви використовуєте для входу в обліковий запис {{appName}}. Будь ласка, натисніть на кнопку нижче, щоб підтвердити нову електронну адресу.",
"verify_email_change_success_toast": "Оновлено вашу електронну пошту до {{email}}",
"verify_email_change_failure_toast": "Не вдалося оновити адресу електронної пошти.",
"change_of_email": "Підтвердьте вашу нову адресу електронної пошти для {{appName}}",
"change_of_email_toast": "Ми надіслали посилання для підтвердження на {{email}}. Ми оновимо адресу електронної пошти після натискання цього посилання.",
"copy_somewhere_safe": "Збережіть цей ключ API у надійному місці. Ви не зможете знову його переглянути.",
"verify_email_email_body": "Підтвердьте свою електронну адресу, натиснувши кнопку нижче.",
"verify_email_by_code_email_body": "Підтвердьте свою електронну адресу, використавши нижчезазначений код.",
@@ -56,6 +63,17 @@
"a_refund_failed": "Не вдалося повернути кошти",
"awaiting_payment_subject": "Очікування платежу: {{title}}, {{date}}",
"meeting_awaiting_payment": "Ваша нарада очікує на оплату",
"dark_theme_contrast_error": "Колір темної теми не проходить перевірку контрасту. Ми рекомендуємо змінити цей колір, щоб його було краще бачити.",
"light_theme_contrast_error": "Колір темної теми не проходить перевірку контрасту. Ми рекомендуємо змінити цей колір, щоб його було краще бачити.",
"payment_not_created_error": "Не вдалося створити платіж",
"couldnt_charge_card_error": "Не вдалося отримати оплату з картки",
"no_available_users_found_error": "Не знайдено жодного доступного користувача. Чи могли б ви спробувати інший часовий слот?",
"request_body_end_time_internal_error": "Внутрішня помилка. Тіло запиту не містить кінцевого часу",
"create_calendar_event_error": "Не вдалося створити подію у календарі організаторів",
"update_calendar_event_error": "Неможливо оновити подію Календаря.",
"delete_calendar_event_error": "Неможливо оновити подію Календаря.",
"already_signed_up_for_this_booking_error": "Ви вже зареєстровані в цьому бронювання.",
"hosts_unavailable_for_booking": "Деякі хости недоступні для бронювання.",
"help": "Довідка",
"price": "Ціна",
"paid": "Сплачено",
@@ -101,6 +119,7 @@
"requested_to_reschedule_subject_attendee": "Потрібно перенести бронювання: забронюйте новий час для заходу «{{eventType}}» із користувачем {{name}}",
"hi_user_name": "Привіт, {{name}}!",
"ics_event_title": "{{eventType}} «{{name}}»",
"please_book_a_time_sometime_later": "Ой, ми не можемо підключитися до вас цього разу. Будь ласка, організуйте майбутній дзвінок замість цього",
"new_event_subject": "Новий захід: {{attendeeName}} – {{date}} – {{eventType}}",
"join_by_entrypoint": "Приєднатися через {{entryPoint}}",
"notes": "Примітки",
@@ -117,15 +136,20 @@
"meeting_id": "Ідентифікатор наради",
"meeting_password": "Пароль наради",
"meeting_url": "URL-адреса наради",
"meeting_url_not_found": "Посилання на зустріч не знайдено",
"token_not_found": "Токен не знайдено",
"some_other_host_already_accepted_the_meeting": "Деякі інші хости вже прийняли зустріч. Ви все ще хочете приєднатися? <1>Продовжити зустріч</1>",
"meeting_request_rejected": "Ваш запит на нараду відхилено",
"rejected_event_type_with_organizer": "Відхилено: {{eventType}} «{{organizer}}», {{date}}",
"hi": "Привіт",
"join_team": "Увійти в команду",
"manage_this_team": "Керуйте цією командою",
"team_info": "Про команду",
"join_meeting": "Приєднатися до зустрічі",
"request_another_invitation_email": "Якщо ви не хочете використовувати {{toEmail}} в {{appName}} або вже маєте обліковий запис {{appName}}, надішліть запит на запрошення на потрібну електронну адресу.",
"you_have_been_invited": "Вас запрошено приєднатися до команди «{{teamName}}»",
"user_invited_you": "{{user}} запрошує вас приєднатись: {{entity}} «{{team}}» в {{appName}}",
"user_invited_you_to_subteam": "{{user}} запросив вас приєднатися до команди {{team}} організації {{parentTeamName}} {{appName}}",
"hidden_team_member_title": "У цій команді вас приховано",
"hidden_team_member_message": "Ваше місце не оплачено. Перейдіть на версію Pro або попросіть власника команди оплатити ваше місце.",
"hidden_team_owner_message": "Щоб користуватися функціями команди, потрібен обліковий запис версії Pro. До переходу на цю версію вас буде приховано.",
@@ -229,6 +253,7 @@
"reset_your_password": "Установіть новий пароль, дотримуючись інструкцій, надісланих на вашу електронну адресу.",
"email_change": "Увійдіть у систему за допомогою нової електронної адреси й пароля.",
"create_your_account": "Створіть власний обліковий запис",
"create_your_calcom_account": "Створіть обліковий запис Cal.com",
"sign_up": "Реєстрація",
"youve_been_logged_out": "Ви вийшли із системи",
"hope_to_see_you_soon": "Чекатимемо на ваше повернення!",
@@ -266,6 +291,9 @@
"nearly_there_instructions": "І останнє: стислі відомості про вас і ваше фото заохочуватимуть інших бронюванювати ваш час, адже вони бачитимуть, з ким домовляються.",
"set_availability_instructions": "Визначте повторювані діапазони часу, у які ви доступні. Ви зможете додати більше таких діапазонів пізніше та призначити їх різним календарям.",
"set_availability": "Укажіть, коли ви доступні",
"set_availbility_description": "Встановіть розклад за яким ви хочете щоб вас бронювати.",
"share_a_link_or_embed": "Поділитись посиланням або вбудованим вмістом",
"share_a_link_or_embed_description": "Поділитись вашим {{appName}} посиланням або вбудуйте його на сайті.",
"availability_settings": "Налаштування доступності",
"continue_without_calendar": "Продовжити без календаря",
"continue_with": "Продовжити з {{appName}}",
@@ -281,6 +309,7 @@
"welcome_to_calcom": "Вітаємо в {{appName}}",
"welcome_instructions": "Скажіть, як вас звати та в якому ви часовому поясі. Ці дані можна буде змінити пізніше.",
"connect_caldav": "Підключитися до CalDav (бета)",
"connect_ics_feed": "Підключитися до ICS стрічки",
"connect": "Підключитися",
"try_for_free": "Спробувати безкоштовно",
"create_booking_link_with_calcom": "Створіть власне посилання на бронювання в {{appName}}",
@@ -419,6 +448,7 @@
"browse_api_documentation": "Переглянути нашу документацію з API",
"leverage_our_api": "Використовуйте наш інтерфейс API, щоб отримати повний контроль і можливості налаштування.",
"create_webhook": "Створити вебгук",
"instant_meeting": "Миттєву зустріч створено",
"booking_cancelled": "Бронювання скасовано",
"booking_rescheduled": "Бронювання перенесено",
"recording_ready": "Посилання для завантаження запису готове",
@@ -533,6 +563,7 @@
"enter_number_between_range": "Введіть число від 1 до {{maxOccurences}}",
"email_address": "Електронна адреса",
"enter_valid_email": "Введіть дійсну адресу електронної пошти",
"please_schedule_future_call": " Якщо ви не доступні через {{seconds}} секунд, будь ласка, сплануйте новий дзвінок",
"location": "Розташування",
"address": "Адреса",
"enter_address": "Введіть адресу",
@@ -592,6 +623,7 @@
"new_member": "Новий учасник",
"invite": "Запросити",
"add_team_members": "Додати учасників команди",
"add_org_members": "Додати учасників",
"add_team_members_description": "Запросіть когось у свою команду",
"add_team_member": "Додати учасника команди",
"invite_new_member": "Запросити нового учасника команди",
@@ -2061,6 +2093,7 @@
"workflow_automation": "Автоматизація робочого процесу",
"scheduling_for_your_team": "Автоматизація робочого процесу",
"no_members_found": "Учасників не знайдено",
"directory_sync": "Directory Sync",
"event_setup_length_error": "Налаштування заходу: мінімальна тривалість — 1 хвилина.",
"availability_schedules": "Розклад доступності",
"unauthorized": "Неавторизовано",
@@ -2068,6 +2068,7 @@
"workflow_automation": "Tự động hoá dòng công việc",
"scheduling_for_your_team": "Tự động hoá dòng công việc",
"no_members_found": "Không tìm thấy thành viên nào",
"directory_sync": "Đồng bộ thư mục",
"event_setup_length_error": "Thiết lập sự kiện: Thời lượng phải ít nhất 1 phút.",
"availability_schedules": "Lịch trống",
"unauthorized": "Chưa được uỷ quyền",
@@ -13,10 +13,16 @@
"reset_password_subject": "{{appName}}: 重置密码教程",
"verify_email_subject": "{{appName}}:验证您的账户",
"check_your_email": "查看您的电子邮件",
"new_email_address": "新电子邮箱地址",
"verify_email_page_body": "我们已向 {{email}} 发送了一封电子邮件。请务必验证您的电子邮件地址,以确保让 {{appName}} 发挥最佳的电子邮件和日历递送能力。",
"verify_email_banner_body": "验证您的电子邮件地址以确保最佳的电子邮件和日历递送能力",
"verify_email_email_header": "验证您的电子邮件地址",
"verify_email_email_button": "验证电子邮件",
"verify_email_change_description": "您最近更改了登陆您的帐户 {{appName}} 时所使用的电子邮箱地址。 请点击下方按钮以确认您的新邮箱地址。",
"verify_email_change_success_toast": "将您的电子邮箱地址变更为 {{email}}",
"verify_email_change_failure_toast": "更新电子邮箱地址时出现了错误。",
"change_of_email": "请确认您为您的帐户 {{appName}} 设置的新的邮箱地址",
"change_of_email_toast": "我们向您的电子邮箱 {{email}} 发送了一封确认邮件。 在您点击了确认邮件中的链接后, 我们将为您更新您的电子邮箱地址。",
"copy_somewhere_safe": "将此 API 密钥保存在安全的地方。您将无法再次查看它。",
"verify_email_email_body": "请点击下面的按钮来验证您的电子邮件地址。",
"verify_email_by_code_email_body": "请使用下面的代码验证您的电子邮件地址。",
@@ -56,10 +62,15 @@
"a_refund_failed": "退款失败",
"awaiting_payment_subject": "等待付款:{{title}} 于 {{date}}",
"meeting_awaiting_payment": "您有会议待付款",
"dark_theme_contrast_error": "暗黑模式未能通过对比度检测。我们建议您修改当前的颜色设置以便使您的按钮更容易分辨。",
"light_theme_contrast_error": "浅色模式未能通过对比度检测。我们建议您修改当前的颜色设置以便使您的按钮更容易分辨。",
"payment_not_created_error": "付款创建失败",
"couldnt_charge_card_error": "无法修改支付卡",
"no_available_users_found_error": "找不到可用时间。请尝试另一个时间段。",
"request_body_end_time_internal_error": "内部错误。请求主体不包含结束时间",
"create_calendar_event_error": "无法在组织者的日历中建立新的日历项目",
"update_calendar_event_error": "无法更新日历事件。",
"delete_calendar_event_error": "无法删除日历项目",
"help": "帮助",
"price": "价格",
"paid": "已付款",
@@ -1447,6 +1458,8 @@
"copy_link_to_form": "将链接复制到表格",
"theme": "主题",
"theme_applies_note": "这仅适用于您的公开预约页面",
"app_theme": "仪表板主题",
"app_theme_applies_note": "这仅在您登录仪表板时适用",
"theme_system": "系统默认设置",
"add_a_team": "添加团队",
"add_webhook_description": "当 {{appName}} 上有活动时实时接收会议数据",
@@ -2089,6 +2102,7 @@
"workflow_automation": "工作流程自动化",
"scheduling_for_your_team": "工作流程自动化",
"no_members_found": "未找到成员",
"directory_sync": "目录同步",
"event_setup_length_error": "获得设置:持续时间必须至少为 1 分钟。",
"availability_schedules": "可预约时间表",
"unauthorized": "未经授权",
@@ -2061,6 +2061,7 @@
"workflow_automation": "工作流程自動化",
"scheduling_for_your_team": "工作流程自動化",
"no_members_found": "找不到成員",
"directory_sync": "Directory Sync",
"event_setup_length_error": "活動設定:持續時間至少必須為 1 分鐘。",
"availability_schedules": "可預約時間行程表",
"unauthorized": "未授權",
+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
+72
View File
@@ -13,11 +13,15 @@ import {
getDate,
getGoogleCalendarCredential,
createBookingScenario,
createOrganization,
getOrganizer,
getScenarioData,
} from "../utils/bookingScenario/bookingScenario";
vi.mock("@calcom/lib/constants", () => ({
IS_PRODUCTION: true,
WEBAPP_URL: "http://localhost:3000",
RESERVED_SUBDOMAINS: ["auth", "docs"],
}));
declare global {
@@ -1255,5 +1259,73 @@ describe("getSchedule", () => {
{ dateString: plus3DateString }
);
});
test("getSchedule can get slots of org's member event type when orgSlug, eventTypeSlug passed as input", async () => {
const org = await createOrganization({ name: "acme", slug: "acme" });
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
// So, that it picks the first schedule from the list
defaultScheduleId: null,
organizationId: org.id,
// Has morning shift with some overlap with morning shift
schedules: [TestData.schedules.IstWorkHours],
});
const scenario = await createBookingScenario(
getScenarioData(
{
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
},
],
organizer,
},
{ id: org.id }
)
);
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const getScheduleRes = await getSchedule({
input: {
eventTypeSlug: scenario.eventTypes[0]?.slug,
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
orgSlug: "acme",
usernameList: [organizer.username],
},
});
expect(getScheduleRes).toHaveTimeSlots(
[
`04:00:00.000Z`,
`04:45:00.000Z`,
`05:30:00.000Z`,
`06:15:00.000Z`,
`07:00:00.000Z`,
`07:45:00.000Z`,
`08:30:00.000Z`,
`09:15:00.000Z`,
`10:00:00.000Z`,
`10:45:00.000Z`,
`11:30:00.000Z`,
],
{ dateString: plus2DateString }
);
});
});
});
@@ -112,6 +112,7 @@ describe("handleChildrenEventTypes", () => {
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
...evType
} = mockFindFirstEventType({
id: 123,
@@ -162,6 +163,7 @@ describe("handleChildrenEventTypes", () => {
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
@@ -260,6 +262,7 @@ describe("handleChildrenEventTypes", () => {
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
...evType
} = mockFindFirstEventType({
id: 123,
@@ -310,6 +313,7 @@ describe("handleChildrenEventTypes", () => {
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
@@ -364,6 +368,7 @@ describe("handleChildrenEventTypes", () => {
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
@@ -9,6 +9,7 @@ import type Stripe from "stripe";
import type { getMockRequestDataForBooking } from "test/utils/bookingScenario/getMockRequestDataForBooking";
import { v4 as uuidv4 } from "uuid";
import "vitest-fetch-mock";
import type { z } from "zod";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { handleStripePaymentSuccess } from "@calcom/features/ee/payments/api/webhook";
@@ -20,6 +21,7 @@ import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import type { WorkflowActions, WorkflowTemplates, WorkflowTriggerEvents } from "@calcom/prisma/client";
import type { SchedulingType } from "@calcom/prisma/enums";
import type { BookingStatus } from "@calcom/prisma/enums";
import type { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { userMetadataType } from "@calcom/prisma/zod-utils";
import type { AppMeta } from "@calcom/types/App";
import type { NewCalendarEventType } from "@calcom/types/Calendar";
@@ -620,11 +622,19 @@ export async function createBookingScenario(data: ScenarioData) {
};
}
export async function createOrganization(orgData: { name: string; slug: string }) {
export async function createOrganization(orgData: {
name: string;
slug: string;
metadata?: z.infer<typeof teamMetadataSchema>;
}) {
const org = await prismock.team.create({
data: {
name: orgData.name,
slug: orgData.slug,
metadata: {
...(orgData.metadata || {}),
isOrganization: true,
},
},
});
return org;
@@ -37,6 +37,7 @@ export const getSampleUserInSession = function () {
disableImpersonation: false,
organizationId: null,
theme: "",
appTheme: "",
createdDate: new Date(),
trialEndsAt: new Date(),
completedOnboarding: false,
+7 -3
View File
@@ -4,12 +4,15 @@
ARG NODE_VERSION=20.7.0
FROM node:${NODE_VERSION}-slim as base
ARG DATABASE_URL
# Next.js/Prisma app lives here
WORKDIR /app
# Set production environment
ENV NODE_ENV="production"
ENV NODE_ENV="production" \
DATABASE_URL=$DATABASE_URL \
DATABASE_DIRECT_URL=$DATABASE_URL
# Throw-away build stage to reduce size of final image
FROM base as build
@@ -21,6 +24,7 @@ COPY ./apps/api ./apps/api
COPY ./packages ./packages
# TODO: follow up pr to remove dependencies on web
COPY ./apps/web ./apps/web
COPY ./tests ./tests
# Install node modules and dependencies, prune unneeded deps, then build
RUN set -eux; \
@@ -66,4 +70,4 @@ COPY --from=build /app/apps/web/package.json ./apps/web/package.json
EXPOSE 80
# Start cmd, called when docker image is mounted
CMD [ "yarn", "workspace", "@calcom/api", "docker-start-api"]
CMD [ "yarn", "workspace", "@calcom/api", "docker-start-api"]
@@ -1,13 +1,25 @@
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
import {
APP_CREDENTIAL_SHARING_ENABLED,
CREDENTIAL_SYNC_SECRET,
CREDENTIAL_SYNC_SECRET_HEADER_NAME,
} from "@calcom/lib/constants";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const refreshOAuthTokens = async (refreshFunction: () => any, appSlug: string, userId: number | null) => {
// Check that app syncing is enabled and that the credential belongs to a user
if (APP_CREDENTIAL_SHARING_ENABLED && process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT && userId) {
if (
APP_CREDENTIAL_SHARING_ENABLED &&
process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT &&
CREDENTIAL_SYNC_SECRET &&
userId
) {
// Customize the payload based on what your endpoint requires
// The response should only contain the access token and expiry date
const response = await fetch(process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT, {
method: "POST",
headers: {
[CREDENTIAL_SYNC_SECRET_HEADER_NAME]: CREDENTIAL_SYNC_SECRET,
},
body: new URLSearchParams({
calcomUserId: userId.toString(),
appSlug,
@@ -13,6 +13,7 @@ import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
import { appKeysSchema as gtm_zod_ts } from "./gtm/zod";
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod";
import { appKeysSchema as intercom_zod_ts } from "./intercom/zod";
import { appKeysSchema as jelly_zod_ts } from "./jelly/zod";
import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
import { appKeysSchema as make_zod_ts } from "./make/zod";
@@ -53,6 +54,7 @@ export const appKeysSchemas = {
gtm: gtm_zod_ts,
hubspot: hubspot_zod_ts,
intercom: intercom_zod_ts,
jelly: jelly_zod_ts,
jitsivideo: jitsivideo_zod_ts,
larkcalendar: larkcalendar_zod_ts,
make: make_zod_ts,

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