Compare commits

..
328 Commits
Author SHA1 Message Date
Alex van Andel 9c389c307c v3.1.5
release.yml / release (push) Canceled after 0s
2023-07-26 17:37:11 +01:00
Leo Giovanetti dae4627ada chore: Skipping totp code for org dev (#10403) 2023-07-26 18:15:37 +02:00
Hariom BalharaandJoe Au-Yeung 83d98a1ac4 fix: Apps (#10394)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-07-26 17:43:33 +02:00
Leo Giovanetti bd40dcfa4d fix: Missing subteam URL [CAL-2240] (#10399) 2023-07-26 17:41:38 +02:00
nicktrn 9dc4c84cfd fix: make husky install compatible with yarn v3 (#10393) 2023-07-26 13:00:32 +01:00
Udit TakkarandAlex van Andel 69c01b69f6 fix: responsive upload avatar (#10392)
* fix: responsive upload avatar

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Update ImageUploader.tsx

Move title to the DialogContent title prop instead of the content.

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-26 11:52:43 +00:00
sean-brydonandAlex van Andel 424edc7437 fix: app.cal.com signup changes (#10369)
* Adds premium username check

* Refactor

* fix usernames

* Return a response so the request does not stall

* Fix conditional

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-26 12:46:25 +01:00
Anik Dhabal BabuandPeer Richelsen 50d2dad62c fix: Toast Onclose is removing specific toast (#9960)
* toast behaviour fixed

* Update showToast.tsx

* Adjust E2E test for toast

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-25 22:27:14 +00:00
Crowdin Bot 06dcaaef3b New Crowdin translations by Github Action 2023-07-25 22:03:48 +00:00
sean-brydonandzomars ad3bdbbda7 fix: Feature flag user cache (#10359)
Co-authored-by: zomars <zomars@me.com>
2023-07-25 22:00:21 +00:00
533371da20 add missing translation (#10380)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-25 22:58:26 +01:00
mohammed hussamandAlex van Andel 94b4507901 feat: dynamic group links + flexible schedule (#9931)
* redirect user to /[user]/[type] for dynamic booking in /[user]

* feat: dynamic group links

* fix: type errors

* fix: more type errors

* Booker components cannot rely on useRouter

* Rename 15min/30/60 to dynamic

* Fixed a blocking test, unclear what it was waiting for

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-25 22:48:09 +01:00
Richard PoelderlandAlex van Andel 829a103f6b chore: enable i18n-unused GH action in website (#10296)
* regen yarn.lock?

* chore: enable i18n-unused GH action in `website`

* Generated yarn.lock with submodules

* Added yarn.lock auth

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-25 18:30:18 +01:00
Udit TakkarandLeo Giovanetti db80afce40 fix: show event type setup tab in managed event types [CAL-2225] (#10367)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-07-25 19:07:09 +02:00
6b9d2331fc fix: Managed Event Type Webhooks (#10300)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-07-25 13:05:02 -04:00
Crowdin Bot 2011a1bd38 New Crowdin translations by Github Action 2023-07-25 16:58:14 +00:00
Leo Giovanetti 6a16366941 fix: Username check for sign-up with invitation in org context (#10375) 2023-07-25 18:54:49 +02:00
Crowdin Bot 5a9ee2047f New Crowdin translations by Github Action 2023-07-25 11:40:25 +00:00
Hariom Balhara 3a80c82023 chore: make Embed and AppStore Tests required (#10358)
* Now, all tests are required

* Add matrix support
2023-07-25 13:37:29 +02:00
Crowdin Bot 1d94552f90 New Crowdin translations by Github Action 2023-07-25 10:51:22 +00:00
Crowdin Bot 620a485fd0 New Crowdin translations by Github Action 2023-07-25 10:48:44 +00:00
Keith Williams eb93e81baf chore: Updated Docker release workflow to use environment variable (#10345) 2023-07-25 16:15:56 +05:30
Hariom Balhara 47739a7636 Fix booker top margin embed (#10357) 2023-07-25 10:58:47 +01:00
Crowdin Bot 10bd332405 New Crowdin translations by Github Action 2023-07-25 09:31:46 +00:00
Hariom BalharaandLeo Giovanetti 35be4182e9 fix: Routing Form and other org related fixes (#10320)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-07-25 14:58:57 +05:30
Crowdin Bot 30095d0428 New Crowdin translations by Github Action 2023-07-25 08:42:40 +00:00
Crowdin Bot 5a9084332f New Crowdin translations by Github Action 2023-07-25 08:39:46 +00:00
Crowdin Bot d897e72ce2 New Crowdin translations by Github Action 2023-07-25 08:37:04 +00:00
Crowdin Bot 582a545e67 New Crowdin translations by Github Action 2023-07-25 08:34:18 +00:00
Crowdin Bot 6340cb9de1 New Crowdin translations by Github Action 2023-07-25 08:31:53 +00:00
Crowdin Bot 89107d82e7 New Crowdin translations by Github Action 2023-07-25 08:29:02 +00:00
Syed Ali Shahbaz 1851a3c1cf update responses.location (#10356) 2023-07-25 10:26:11 +02:00
Crowdin Bot 53cda97c6e New Crowdin translations by Github Action 2023-07-25 06:58:46 +00:00
Crowdin Bot f92f0ce1fa New Crowdin translations by Github Action 2023-07-25 06:55:54 +00:00
Crowdin Bot 018eb59c9d New Crowdin translations by Github Action 2023-07-25 06:52:57 +00:00
Crowdin Bot 03d8ef9c03 New Crowdin translations by Github Action 2023-07-25 06:49:57 +00:00
Crowdin Bot d10071a4a2 New Crowdin translations by Github Action 2023-07-25 06:46:57 +00:00
Crowdin Bot 27aaf3f75a New Crowdin translations by Github Action 2023-07-25 06:43:58 +00:00
Crowdin Bot 6ff4e07d8f New Crowdin translations by Github Action 2023-07-25 06:41:19 +00:00
Crowdin Bot b740423eb8 New Crowdin translations by Github Action 2023-07-25 06:38:46 +00:00
Crowdin Bot c0f21be255 New Crowdin translations by Github Action 2023-07-25 06:35:38 +00:00
Joe Au-YeungandOmar López 0e62c411f8 fix: Conferencing Label When Installing Apps (#10351)
Co-authored-by: Omar López <zomars@me.com>
2023-07-25 12:03:00 +05:30
Crowdin Bot c89b217c92 New Crowdin translations by Github Action 2023-07-24 21:13:57 +00:00
Leo Giovanetti d3bbe8784f Onboarding wizard and verif email (#10346) 2023-07-24 18:11:25 -03:00
Afrie Irham f168ec8df6 fix: [CAL-2040] add spinner when bookerState is loading (#9873) 2023-07-24 13:20:42 -04:00
Alex van Andel 0138fdd971 feat: Don't run middleware on _next, .*avatar.png & favicon.ico (#10344) 2023-07-24 09:55:55 -07:00
Colin GriffinandKeith Williams 365b95e28a chore: Add remote calcom/docker release workflow (#9841)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-24 18:09:12 +02:00
sean-brydon 0ea76332db fix: organization list members error (#10340) 2023-07-24 15:01:16 +00:00
Alex van Andel 43e3ef170a fix: webhook fetch calls should be no-follow (#10336) 2023-07-24 07:36:45 -07:00
Crowdin Bot 9202b53e49 New Crowdin translations by Github Action 2023-07-24 14:17:39 +00:00
Meet Patel 41850c7f59 fix: routing-forms protected with LicenseRequired (#9521) 2023-07-24 19:44:28 +05:30
bd4f99a850 fix: Fixed Issue #10291 removed brand color for tooltip (#10327)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bhargav Tenali <t.bhargav@kayrros.com>
2023-07-24 15:21:18 +02:00
Crowdin Bot 771f0b67db New Crowdin translations by Github Action 2023-07-24 11:42:34 +00:00
Vachmara 0f0b55488f fix: app store cli windows compatibility issue #10324 (#10326)
* fix: app-store-cli windows support issue #10324

* fix: app-store-cli windows support issue #10324 - remove yarn.lock

* fix: app-store-cli windows support issue #10324

* refactor: instead of repeating code, use constant to know if it is windows platform
2023-07-24 13:39:28 +02:00
Hariom Balharaandsean-brydon 4e1e586541 fix: nameless in sidebar for orgs (#10318)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-07-24 10:25:55 +00:00
80a1458538 fix: Update ImageUploader.tsx (#10319)
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-24 10:13:58 +00:00
8de75aebf0 feat: Added feature of embeding availability to emails (#10183)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-07-24 10:09:03 +00:00
Aditya Nikam a9cc3649e0 fix: fixed typo email res.statusCode(200) (#10331) 2023-07-24 11:55:51 +02:00
GitStart-Cal.comandgitstart-calcom 2b5577772b fix: Fix UI issues on embed popup footer (#10286)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-24 11:47:25 +05:30
Hariom Balhara f081c733b8 test: Add Embed close modal test (#10323)
* Add close modal test

* Update packages/embeds/embed-core/playwright/lib/testUtils.ts
2023-07-23 12:45:05 +00:00
Hariom Balhara 3ed5a5552a fix: Fix embed iframe still visible even after closing the embed. (#10321)
* Reset visibility

* Update packages/embeds/embed-core/src/embed.ts
2023-07-23 10:55:40 +00:00
Hariom Balhara 61f1a3b21e feat: Support adding links to checkbox type question using markdown[Blocked on another PR] (#8943)
* Support adding links in boolean checkbox

* Fix and comment

* remove textarea, it was accidentally pushed.
2023-07-21 23:30:28 +05:30
d353904c7c test: Create unit tests for react components in packages/ui/components/dialog (#10273)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-21 18:54:57 +02:00
Crowdin Bot 8e192ac358 New Crowdin translations by Github Action 2023-07-21 16:47:12 +00:00
GitStart-Cal.comandgitstart-calcom 85c3c6dfc0 test: Create unit tests for react components in packages/ui/components/credits (#10044)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-21 18:44:12 +02:00
1aa3077706 test: e2e for workflows (#9657)
* create test files

* add first e2e test

* finish first e2e test

* code clean up

* fix event type create button data-testid

* enable sandbox mode for non production environments

* comment out code for testing

* Revert "comment out code for testing"

This reverts commit 5553dacce23cd85c27e41b76f99fc31ce69499b7.

* clean up comments

* prettier

* use NEXT_PUBLIC_IS_E2E

* use todo fn

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-07-21 21:06:39 +05:30
Crowdin Bot 337192697f New Crowdin translations by Github Action 2023-07-21 15:18:41 +00:00
Hariom Balhara 299f74c1fb name can be undefined for existing name type fields (#10298) 2023-07-21 15:14:39 +00:00
d55404b3aa test: Create unit tests for react components in packages/ui/components/errorBoundary (#10275)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-21 14:22:17 +02:00
Shivam Kalraandalannnc 1bb5b0f76c test: team booking (#10267)
Co-authored-by: alannnc <alannnc@gmail.com>
2023-07-21 13:58:52 +02:00
Hariom Balhara 2a8eca7885 chore: Update version in main for embeds that are already released. (#9822)
* Release latest embeds

* Release latest embeds

* Ensure that unpublished packages arent imported

* linter is important to catch unexpected imports

* Create new release

* Add a new patch
2023-07-21 16:46:18 +05:30
Alex van Andel 1566a80e40 v3.1.4
release.yml / release (push) Canceled after 0s
2023-07-21 10:02:01 +01:00
Nafees NazikandCarina Wollendorfer 797d3a7f98 fix: set up organization field validation (#10154)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-07-21 08:48:43 +00:00
c7a276eb1d fix: remove app dialog while removing installed calendar (#10234)
* Fix remove app dialog while removing installed calendar

* fix: dialog in webhook, otp

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: more dialogs

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: margin

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-21 08:46:16 +00:00
Crowdin Bot 1709e23135 New Crowdin translations by Github Action 2023-07-21 08:35:03 +00:00
Leo Giovanetti d1778fdce8 fix: 2FA from CAL to GOOGLE (#10288)
* Fixing 2FA from CAL to GOOGLE

* Missing change

* Missing logic for converted providers

* Fixing edge case and applying feedback

* Leftovers
2023-07-21 09:32:03 +01:00
Joe Au-Yeung b49952b8d8 Fix prettier error on App component (#10287) 2023-07-21 09:31:02 +01:00
Alex van Andel 5b280b2188 fix: team event emails; sending & email subject (#10289) 2023-07-20 19:29:08 -04:00
Alex van Andel 3818093652 fix: Add attendees to getLuckyUser algorithm (#10285)
* Add attendees to getLuckyUser algorithm

* Fix unit test for getLuckyUser
2023-07-20 22:51:15 +01:00
Crowdin Bot 56f4310c24 New Crowdin translations by Github Action 2023-07-20 20:31:50 +00:00
Peer Richelsen 7ec7195ae9 fixed empty screen width (#10278) 2023-07-20 20:29:05 +00:00
Joe Au-Yeung 514db3466f fix: Flaky 2FA tests (#10283) 2023-07-20 19:40:35 +00:00
alannncandPeer Richelsen cd88b688f4 fix: including parentId when filter by eventTypeId happens (#10262)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-20 12:19:13 -07:00
Omar López 939a32813f Revert "chore: Make embed and app-store tests required" (#10282) 2023-07-20 11:58:07 -07:00
Joe Au-Yeung 66c1e29d91 fix: Default to Daily if Viable (#10279)
* Default to daily if viable

* Add Zod schema parsing
2023-07-20 12:57:10 -04:00
Crowdin Bot 3c57082fa6 New Crowdin translations by Github Action 2023-07-20 12:10:47 +00:00
Hariom Balhara fc4a2bd186 make embed and app-store tests required (#10271) 2023-07-20 14:08:09 +02:00
Hariom Balhara 3f900966b0 Fix flash of non-transparent background (#10265) 2023-07-20 09:17:46 +02:00
Leo GiovanettiandOmar López 8ba4b4d50e feat: Pending members selection (#10168)
* Pending members selection

* Discard changes to .vscode/launch.json

* revert yarn.lock

---------

Co-authored-by: Omar López <zomars@me.com>
2023-07-20 11:23:15 +05:30
GitStart-Cal.comandgitstart-calcom d2104dac5a test: Create unit tests for react components in packages/ui/components/breadcrumb (CALCOM-614) (#10256)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-20 07:13:23 +02:00
GitStart-Cal.comandgitstart-calcom 9ad2731358 test: add unit tests for packages/ui/components/badge (CALCOM-613) (#10255)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-20 07:11:53 +02:00
GitStart-Cal.comandgitstart-calcom c34fdab5af test: Create unit tests for react components in packages/ui/components/card (CALCOM-617) (#10257)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-20 07:06:15 +02:00
Hariom BalharaandCarina Wollendorfer f1c981fdcf feat: firstAndLastName variant for name field (#8671)
* Add split full name variant

* Share propsType across fieldTypes and components

* Simplify Components

* Add assertions for required field indicator

* Fix test as name cant be used as a custom field right now

* Make it disabled during reschedule

* Fix UI issues in dark mode

* Support adding links in boolean checkbox

* Revert "Support adding links in boolean checkbox"

This reverts commit 31252f8a5f445f8289a09445124109a4a9c3d33b.

* Make sure getBookingFields isnt import client side

* PR feedback addressed from Carina

* DRY Code

* Fix Dialog

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-07-20 07:03:50 +02:00
Anik Dhabal Babu 47f65bb19f fix: More Brand color for calendar view (#10252) 2023-07-20 00:32:25 +02:00
GitStart-Cal.comandgitstart-calcom 587d3c1088 fix: duplicate event-type dialog not using the new dialog footer (CALCOM-10258) (#10260)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-20 00:15:03 +02:00
sean-brydon 610c86837a feat: orgs - user management v2
figma: https://www.figma.com/file/ZJNAFHCAq6rnuGRgUFxjzQ/Organizations----New-features?type=design&node-id=4304-330348&mode=design&t=nKonHZpucwxnqwQI-4 (Not all features are in this PR)
loom: https://www.loom.com/share/30e8ef20a5b34a708dca160d8c04ae6f

shoutout @shadcn for the base of this table <3 

Member View: 
![CleanShot 2023-07-14 at 14 09 02](https://github.com/calcom/cal.com/assets/55134778/5db6eedb-bece-4955-91f0-9f2b3b042300)

Admin view:
![CleanShot 2023-07-14 at 14 09 07](https://github.com/calcom/cal.com/assets/55134778/708c624d-1990-4075-8b32-d4587d09deff)

How to test 
- Create ORG
- add members
- view settings -> orgs -> members
2023-07-19 12:45:13 -07:00
Crowdin Bot 676452b4f4 New Crowdin translations by Github Action 2023-07-19 18:47:31 +00:00
Carina WollendorferandCarinaWolli 8a432b45a4 fix: issue with date overrides in some timezones (#10254)
* fix date overrides

* add test

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-07-19 19:44:41 +01:00
Crowdin Bot 92ad652666 New Crowdin translations by Github Action 2023-07-19 16:15:23 +00:00
Mingjie Jiang 617ddcaa3b chore: Improve and standardize simplified and traditional Chinese localizations (#9902) 2023-07-19 16:12:09 +00:00
Crowdin Bot ef7f3e7900 New Crowdin translations by Github Action 2023-07-19 16:10:58 +00:00
4e0a549a50 fix: Fix bookingPage UI issues in dark mode (#10203)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-07-19 16:07:07 +00:00
Crowdin Bot 8f95d27469 New Crowdin translations by Github Action 2023-07-19 15:44:41 +00:00
8902f7efa1 fix: /event-type /apps - doesn't match designs (CALCOM-9180) (#9549)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-19 17:41:37 +02:00
GitStart-Cal.comandgitstart-calcom bd7eadd721 test: Create unit tests for react components in packages/ui/components/alert (tests-alert-component) (#9897)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-19 17:22:41 +02:00
Crowdin Bot 02e34a7bde New Crowdin translations by Github Action 2023-07-19 15:21:40 +00:00
sean-brydonandShivam Kalra d51e1ab47d fix: Fixes error for unknown JSON when creating an account (#10248)
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-07-19 15:18:31 +00:00
Crowdin Bot ebe60c36bd New Crowdin translations by Github Action
release.yml / release (push) Canceled after 0s
2023-07-19 14:33:31 +00:00
Monto 26e458be6d feat: dynamic time format rendering in workflows based on user settings (#10229) 2023-07-19 10:30:37 -04:00
Crowdin Bot 1ad4f7247e New Crowdin translations by Github Action 2023-07-19 12:37:55 +00:00
Crowdin Bot e0237acac9 New Crowdin translations by Github Action 2023-07-19 12:35:34 +00:00
Crowdin Bot 2dc0bb4888 New Crowdin translations by Github Action 2023-07-19 12:33:09 +00:00
sean-brydon dfffd8b577 fix: revert i18n hook changes (#10249) 2023-07-19 12:30:25 +00:00
Crowdin Bot e6f2ba2c25 New Crowdin translations by Github Action 2023-07-19 09:22:27 +00:00
sean-brydonandKeith Williams 628a29e89c chore: fixing some initial re-renders (#10185)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-19 09:19:53 +00:00
Crowdin Bot 2fd9ef2733 New Crowdin translations by Github Action 2023-07-19 09:05:08 +00:00
Crowdin Bot c9f2d6bc93 New Crowdin translations by Github Action 2023-07-19 09:01:24 +00:00
Peer Richelsen a83aea30da Update package.json 2023-07-19 10:58:27 +02:00
Hariom BalharaandShivam Kalra b516a2af44 Get OAuth working for local (#10054)
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-07-19 09:15:05 +02:00
Crowdin Bot d0e831975d New Crowdin translations by Github Action 2023-07-19 06:40:31 +00:00
Crowdin Bot b906c7caf7 New Crowdin translations by Github Action 2023-07-19 06:37:40 +00:00
Crowdin Bot 152dbe58c9 New Crowdin translations by Github Action 2023-07-19 06:34:49 +00:00
Crowdin Bot b4c154a6af New Crowdin translations by Github Action 2023-07-19 06:32:11 +00:00
Crowdin Bot 99cea7a326 New Crowdin translations by Github Action 2023-07-19 06:29:40 +00:00
Crowdin Bot d0ae519851 New Crowdin translations by Github Action 2023-07-19 06:27:16 +00:00
Crowdin Bot d185c665b8 New Crowdin translations by Github Action 2023-07-19 06:24:21 +00:00
Crowdin Bot baa9ddf46a New Crowdin translations by Github Action 2023-07-19 06:21:52 +00:00
Alex van Andel e8f5090acc Fix: Flaky 2FA test due to waiting redundant Enter (#10236) 2023-07-19 08:19:16 +02:00
Crowdin Bot 0661b96cd2 New Crowdin translations by Github Action 2023-07-19 05:10:16 +00:00
Crowdin Bot 3a64506c9e New Crowdin translations by Github Action 2023-07-19 05:07:31 +00:00
Crowdin Bot f1b705e022 New Crowdin translations by Github Action 2023-07-19 05:05:00 +00:00
Crowdin Bot c768d52539 New Crowdin translations by Github Action 2023-07-19 05:02:24 +00:00
Crowdin Bot 7d3ba5c55c New Crowdin translations by Github Action 2023-07-19 04:59:34 +00:00
Crowdin Bot e643afaa80 New Crowdin translations by Github Action 2023-07-19 04:57:05 +00:00
Crowdin Bot 4cd3479cfd New Crowdin translations by Github Action 2023-07-19 04:54:39 +00:00
Crowdin Bot 7efddfd213 New Crowdin translations by Github Action 2023-07-19 04:51:59 +00:00
Crowdin Bot a8c362dbf3 New Crowdin translations by Github Action 2023-07-19 04:49:27 +00:00
Crowdin Bot f6b3cc0c7c New Crowdin translations by Github Action 2023-07-19 04:46:55 +00:00
Hariom Balhara 902bb71273 Prefill logged in info (#10240) 2023-07-19 06:44:13 +02:00
db021cc696 fix: update list for countries that don't support alphanumeric sender ids (#10232)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-18 21:31:45 +00:00
Jaideep GuntupalliandUdit Takkar a2645c2ed4 feat: replaced slack icon with Discord (#10193)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-07-18 21:30:17 +00:00
Anik Dhabal Babu 1ec9a6924c feddback added (#10221) 2023-07-18 21:29:56 +00:00
Leo Giovanetti 09068b39e6 perf: Event-type config page (#10238) 2023-07-18 23:18:08 +02:00
Crowdin Bot 9069a2412d New Crowdin translations by Github Action 2023-07-18 20:30:45 +00:00
zomars 6b5c8935c5 fix: formatting 2023-07-18 13:27:54 -07:00
Crowdin Bot d27ff41682 New Crowdin translations by Github Action 2023-07-18 19:53:12 +00:00
Crowdin Bot c6ca736daa New Crowdin translations by Github Action 2023-07-18 19:50:00 +00:00
Carina WollendorferandCarinaWolli cedb2b72e9 fix: add missing booking paid webhook trigger to dropdwon (#10237)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-07-18 19:46:53 +00:00
Hariom Balhara 11081cb5d8 fix: Embed - Non transparent background when color-scheme of embedding document and cal.com iframe don't match (#10033)
## What does this PR do?

Fixes #10032

Before:
https://www.loom.com/share/1890e4eebf41426aaf66d8b4909d0765
After:
https://www.loom.com/share/8b00c41587f2467ab05ef48e4ed523ef


## Type of change

<!-- Please delete bullets that are not relevant. -->

- Bug fix (non-breaking change which fixes an issue)

## How should this be tested?

- See loom

## Mandatory Tasks

- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.

## Checklist
- I haven't added tests that prove my fix is effective or that my feature works
2023-07-18 09:46:35 -07:00
Hariom Balhara 34d65f67a9 fix: Avoid resetting state one every render. (#10210) 2023-07-18 16:42:40 +00:00
Keith Williams e4c49d0e0b fix: Added step to create env file for bundle analysis (#10224) 2023-07-18 09:20:17 -07:00
Peer Richelsen 43a30aa51d Update package.json
release.yml / release (push) Canceled after 0s
2023-07-18 18:14:14 +02:00
Jay Pitroda eb13057157 fix: booking error on invalid timezone (#10209) 2023-07-18 16:09:31 +00:00
Syed Ali Shahbaz bb4d587359 fix: lint errors introduced in PR#10225 (#10230) 2023-07-18 15:58:32 +00:00
Crowdin Bot 3d7c32c0d8 New Crowdin translations by Github Action 2023-07-18 15:41:03 +00:00
39508b3daa fix: booking page: location icon is not showing up (CALCOM-9951) (#9993)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-18 15:37:39 +00:00
Richard PoelderlandKeith Williams e79c818edb chore: Add website deps to yarn.lock (#10219)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-18 14:37:16 +00:00
Mehul 8a1023087f fix: inconsistent duration labels (#10223) 2023-07-18 15:10:28 +01:00
Crowdin Bot 9343aee778 New Crowdin translations by Github Action 2023-07-18 13:52:15 +00:00
Syed Ali Shahbaz 89f645c450 fix: Hotfix-bookings list breaks on unsupported timezone (#10225)
* Adds a check for supportedTimezone values

* adds timezone support check for attendees
2023-07-18 13:49:23 +00:00
Crowdin Bot da5d90eb07 New Crowdin translations by Github Action 2023-07-18 11:29:18 +00:00
Crowdin Bot 9f30f95dd9 New Crowdin translations by Github Action 2023-07-18 11:26:41 +00:00
Crowdin Bot bc0c1341f6 New Crowdin translations by Github Action 2023-07-18 11:24:09 +00:00
Crowdin Bot 1c0a51cc02 New Crowdin translations by Github Action 2023-07-18 11:21:42 +00:00
Crowdin Bot 7cfe7647f3 New Crowdin translations by Github Action 2023-07-18 11:17:58 +00:00
Crowdin Bot cc91834ddd New Crowdin translations by Github Action 2023-07-18 11:14:53 +00:00
Crowdin Bot 2cb1a62906 New Crowdin translations by Github Action 2023-07-18 11:12:13 +00:00
Crowdin Bot 6135d449ec New Crowdin translations by Github Action 2023-07-18 11:09:31 +00:00
Crowdin Bot 073b29c214 New Crowdin translations by Github Action 2023-07-18 11:06:19 +00:00
Crowdin Bot ea99dc5520 New Crowdin translations by Github Action 2023-07-18 11:03:30 +00:00
Crowdin Bot c3280a62b7 New Crowdin translations by Github Action 2023-07-18 11:00:47 +00:00
Crowdin Bot a41b42359a New Crowdin translations by Github Action 2023-07-18 10:57:56 +00:00
Crowdin Bot cc15fb5285 New Crowdin translations by Github Action 2023-07-18 10:55:10 +00:00
Crowdin Bot e554268592 New Crowdin translations by Github Action 2023-07-18 10:52:35 +00:00
Crowdin Bot f4c2764cbf New Crowdin translations by Github Action 2023-07-18 10:49:58 +00:00
Crowdin Bot ac54ccdd70 New Crowdin translations by Github Action 2023-07-18 10:47:26 +00:00
Hariom BalharaandShivam Kalra b3216634c9 fix: Add missing auth package to yarn.lock (#10214)
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-07-18 12:44:38 +02:00
Crowdin Bot 6c437edc0e New Crowdin translations by Github Action 2023-07-18 10:11:17 +00:00
Crowdin Bot bed153b497 New Crowdin translations by Github Action 2023-07-18 10:08:12 +00:00
2be245eefc add CAL_URL to fix og image issue (#10172)
Co-authored-by: Raghul D <v-raghuld@microsoft.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-18 10:05:28 +00:00
Crowdin Bot caf3b40687 New Crowdin translations by Github Action 2023-07-18 09:33:33 +00:00
Crowdin Bot 5650bd6409 New Crowdin translations by Github Action 2023-07-18 09:30:53 +00:00
Crowdin Bot 94a86acdaa New Crowdin translations by Github Action 2023-07-18 09:28:07 +00:00
Crowdin Bot defd49f986 New Crowdin translations by Github Action 2023-07-18 09:25:37 +00:00
Crowdin Bot 679d3da957 New Crowdin translations by Github Action 2023-07-18 09:23:02 +00:00
Crowdin Bot e84fd39e11 New Crowdin translations by Github Action 2023-07-18 09:20:12 +00:00
Crowdin Bot 1d74e9c201 New Crowdin translations by Github Action 2023-07-18 09:17:11 +00:00
Crowdin Bot 3356cbc1df New Crowdin translations by Github Action 2023-07-18 09:13:52 +00:00
Crowdin Bot 4584f666e0 New Crowdin translations by Github Action 2023-07-18 09:10:50 +00:00
Crowdin Bot c9078cfd94 New Crowdin translations by Github Action 2023-07-18 09:07:49 +00:00
Crowdin Bot 4ae1c2aa8c New Crowdin translations by Github Action 2023-07-18 09:05:11 +00:00
Crowdin Bot cf15f5f2a2 New Crowdin translations by Github Action 2023-07-18 09:01:52 +00:00
Crowdin Bot d21f42c257 New Crowdin translations by Github Action 2023-07-18 08:58:31 +00:00
Crowdin Bot 92a0dff2c7 New Crowdin translations by Github Action 2023-07-18 08:55:56 +00:00
Udit Takkar 47add7e7fc fix: modals regression (#10211)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-07-18 09:53:01 +01:00
Hariom Balhara 6dfc19247e fix: Embed theme not working using Embed API (#10163)
## What does this PR do?

Fixes #10187
See [Tests Done](https://www.loom.com/share/f03e0191b60143d8b45a505042dbfa11)

## Type of change
  - [x] Bug fix (non-breaking change which fixes an issue)

## How should this be tested?
- [x] Configure embed to use `dark` theme and verify that dark theme is shown on event booking page(when user has light theme set). This is failing in main
- Additional Tests for embed to avoid any new regression
	- [x] - Configure "auto" theme using embed API and see it reacts to system theme
	- [x] - Don't configure any theme and see that "light" theme is shown even when we switch system theme(Because User has configured light theme in App)
-  Tests outside embed to avoid any new regression
	- [x] - See that light theme is shown even after switching system theme
	- [x] - Now, switch the user theme to dark and see that it reflects the change. 

## Mandatory Tasks

 [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
2023-07-17 18:02:42 -07:00
2db4998eaa fix: wrong slot starting times for half hour times zones (#10161)
* fix timezone zone issue for first slot

* fix failed test in getSchedule

* Update apps/web/test/lib/getSchedule.test.ts

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2023-07-18 00:57:34 +00:00
alannncandLeo Giovanetti dad4fd17fb fix: Improve continue button on org-add-team and validations [CAL-2015] (#10122)
* Improve continue button on team add and validations

* Fix server validations for orgs create team

* Remove unused prop form input

* add comment for context

* Fix duplicatedSlugs not working properly

* Fix continue button disabled state, added more examples for team names

* Fix initial state for inputValues on addTeamForm

* Don't allow whitespaces only inputs on create teams

* Use rhf instead of react useState

---------

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-07-17 17:17:29 -07:00
c26efcabe4 fix: customTemplate() function fails when no locale is given (#10201)
* fallback to en for locale

* remove console.log

* Fix nit changes

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2023-07-17 16:38:37 -07:00
Matthias GemperliandPeer Richelsen 6a8db56dbb fix: locales in dates utils (#10067)
release.yml / release (push) Canceled after 0s
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-17 20:19:00 +00:00
6c058d041a fix: check for single email in TextAreaField (#10149)
Co-authored-by: Dewansh Chaudhari <codewansh@Dewanshs-MacBook-Pro.local>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-17 20:12:12 +00:00
Andrew Milich 07b43e107e feat: Add Skiff to app store (#10079) 2023-07-17 13:07:35 -07:00
Peer Richelsen ab8ad6434f Update package.json 2023-07-17 13:05:08 -07:00
4d7a3ac21a fix: modal footer issue (#10020)
Co-authored-by: Raghul D <v-raghuld@microsoft.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-07-17 13:02:34 -07:00
sean-brydon 2ac709630f feat: Page loading indicator (#10195) 2023-07-17 13:00:44 -07:00
Crowdin Bot 64147d3618 New Crowdin translations by Github Action 2023-07-17 15:39:08 +00:00
alannnc fb0e89ab22 fix: possible to have same slug for team and user in an organization 9985 cal 2122 (#10170)
* Added rule to validate username vs team slugs in team considering orgs too

* Validate valid username vs team slugs

* Add or clause to find org team and child teams
2023-07-17 08:36:14 -07:00
Crowdin Bot 7bee930983 New Crowdin translations by Github Action 2023-07-17 13:30:56 +00:00
Hariom Balhara 9678361f5c fix: Embed/Show header in weekly view and column view (#10192) 2023-07-17 18:58:04 +05:30
sean-brydon a404ba471d Move to minmax instead of auto grid (#10190) 2023-07-17 12:58:30 +00:00
Crowdin Bot 9a8aab2e54 New Crowdin translations by Github Action 2023-07-17 12:20:10 +00:00
Satya NishanthandSatya Nishanth 4c54e9c384 CAL-2126 url layout query param is source of truth for layout (#10100)
Co-authored-by: Satya Nishanth <satya@Satyas-MacBook-Pro.local>
2023-07-17 14:16:50 +02:00
Peer Richelsen 8dd2135656 fix: iw to he (hebrew) (#10184) 2023-07-17 09:57:29 +00:00
Crowdin Bot 4ba72425b9 New Crowdin translations by Github Action 2023-07-17 09:54:27 +00:00
Jaideep GuntupalliandPeer Richelsen f4b0772746 refactor: renamed getMostPopularApps function to getInstallCountPerApp (#10140)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-17 09:51:35 +00:00
Crowdin Bot 1a605b555a New Crowdin translations by Github Action 2023-07-17 03:18:34 +00:00
Pradumn KumarandHariom Balhara 17324e406c feat: unit tests for embed pages (#10014)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-07-17 08:46:00 +05:30
Crowdin Bot 833459cabb New Crowdin translations by Github Action 2023-07-16 21:34:09 +00:00
Peer RichelsenandLeo Giovanetti 6076fd7ba0 reduced duplicate event type to 1 min (#10159)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-07-16 23:31:34 +02:00
Crowdin Bot ca8b380815 New Crowdin translations by Github Action 2023-07-15 16:03:37 +00:00
Omar López d7953aa53a v3.1.0
release.yml / release (push) Canceled after 0s
2023-07-15 09:01:03 -07:00
Joe Au-Yeung 494012484e fix: Add feedback when user installs app [CAL-2144] (#10169)
* Prevent multiple clicks on app store

* Disable multiple clicks on app page

* Disable multiple clicks on app page

* Clean up
2023-07-15 05:43:10 +02:00
Crowdin Bot 8c6669e204 New Crowdin translations by Github Action 2023-07-15 01:22:39 +00:00
Leo Giovanetti d5f3623377 chore: Relying on feature flag (#10162) 2023-07-14 22:20:03 -03:00
Crowdin Bot ef483a6520 New Crowdin translations by Github Action 2023-07-14 23:11:42 +00:00
Crowdin Bot 339f00f6aa New Crowdin translations by Github Action 2023-07-14 23:09:32 +00:00
e98bebb9b2 feat: Zapier For Teams (#9851)
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-07-14 16:06:57 -07:00
Crowdin Bot dcd7e80e6a New Crowdin translations by Github Action 2023-07-14 23:04:48 +00:00
zomars fcc13da23d Update rateLimit.ts 2023-07-14 16:02:10 -07:00
zomars 67a210d439 Sync packages 2023-07-14 16:00:59 -07:00
zomars e813f8bb17 Update rateLimit.ts 2023-07-14 15:59:04 -07:00
Carina WollendorferandCarinaWolli 5ff8d2db6e fix: slot start times with interrupted availabilities (#10164)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-07-14 15:57:05 -07:00
Crowdin Bot 7953551189 New Crowdin translations by Github Action 2023-07-14 17:04:38 +00:00
jemiluv8 879bc3f169 fix: minor bug in schedule sms and whatsapp reminders cron job (#10135) 2023-07-14 19:02:06 +02:00
Purushottam Khedreandsean-brydon 4aaf5da36d fix: no feedback on buttons on /getting-started page. (#10152)
* add isLoading type to ButtonProps type

* add loading feedback

* Update apps/web/components/getting-started/components/AppConnectionItem.tsx

Not neeeded as buttonProps.isLoading is false by default :)

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-07-14 09:03:11 +00:00
alannnc fb16d8e900 Added distinct clause when getting user list in team (#10144) 2023-07-14 00:04:30 +02:00
Afzal SayedandAlex van Andel 58d6bb840a feat: Auto submit 2FA if all inputs have a value (#9729)
* auto submit 2FA if all inputs have a value

* organize imports

* Fix duplicate text issue, this label is part of TwoFactor

* Fixed test that was failing due to enable-2fa not clickable anymore due to auto-submit

* Remove extra newline for clarity

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-13 20:54:57 +00:00
Carina WollendorferandCarinaWolli 1419c534c8 fix: webhooks for events with seats (#10143)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-07-13 22:40:16 +02:00
Pradumn Kumar 4de1de3418 fix: added slugify the eventType slugs on the api aswell (#10007) 2023-07-13 17:33:26 +00:00
Rama Krishna Reddyandrkreddy99 c23a2102f4 remove max width from bookings page (#10126)
Co-authored-by: rkreddy99 <rreddy@e2clouds.com>
2023-07-13 19:17:02 +02:00
Keith Williams df686370ab chore: Update GitHub env caching to use current PR details (#10096) 2023-07-13 19:08:50 +02:00
Alex van Andel 0bc7d4ceda feat: Override any day of the week (#10138)
* feat: Override any day of the week

* Having multiple workingHours would duplicate the default
2023-07-13 15:33:27 +00:00
Hariom Balhara 9444f925e5 fix: Important Booking Page fixes (#10130) 2023-07-13 19:38:41 +05:30
Hariom Balhara 2918a34e1a fix: More instances of Avatar and booker links fixed (#10083) 2023-07-13 19:22:21 +05:30
213caa722e feat: Added order by most popular in Onboarding screen (#9891)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-13 19:17:46 +05:30
Crowdin Bot 6c7d7be9cc New Crowdin translations by Github Action 2023-07-13 13:32:30 +00:00
Nafees Nazikandsean-brydon f1ef50e606 fix: upload csv button not clickable (#9981)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-07-13 15:29:29 +02:00
Alex van Andel 22c51ba483 v3.0.16
release.yml / release (push) Canceled after 0s
2023-07-13 14:40:20 +02:00
Alimurtuza b6eaafa39f fix: "change username" dialog design (#10095) 2023-07-13 14:28:16 +02:00
98707e66e1 fix: Refresh session (#10019)
* Refresh session 🔄

* Update apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx

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

* Update apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx

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

* Update packages/features/auth/lib/next-auth-options.ts

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

---------

Co-authored-by: Ansari <thameem.ns@zohocorp.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-07-13 14:21:28 +02:00
Crowdin Bot 44308cbfa0 New Crowdin translations by Github Action 2023-07-13 12:12:23 +00:00
Purushottam Khedre 352a9e2fec fix: incorrect error messsage for invalid login credentials (#10117)
* fir ErrorCode enum

* update incorrect credentails message
2023-07-13 12:09:19 +00:00
Crowdin Bot c1a9f4b656 New Crowdin translations by Github Action 2023-07-13 12:02:12 +00:00
Leo GiovanettiandShivam Kalra 32c6ca0ad5 fix: Creating a default availability for org owner [CAL-2142] (#10108)
* Creating a default availability for org owner

* Full width empty state

---------

Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-07-13 13:59:12 +02:00
e79b4168f2 fix: Improved recurring event booking speed (#9600)
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-07-13 17:28:20 +05:30
Hariom Balhara 54833b65ff fix: Hotfix/consider new conferencing and old video app categories (#10134)
* Consider new conferencing and old video app categories

* Add comment
2023-07-13 11:57:49 +00:00
Rama Krishna Reddyandrkreddy99 26afc077a7 disable guests field when offer seats is enabled (#10132)
Co-authored-by: rkreddy99 <rreddy@e2clouds.com>
2023-07-13 13:57:30 +02:00
Jaideep Guntupalli 0093b6faa5 feat: Added e2e test paid booking should be able to be rescheduled (#10069)
* feat: Added e2e test paid booking should be able to be rescheduled

* refactor: abstracted and simplied the logic

* fix: stripe iframe isnt the first frame

* fix: import expect
2023-07-13 02:47:06 +02:00
Alex van Andel ef49fc4ce1 fix: Remove/Update behaviour of date-overrides
## What does this PR do?

This is not the cleanest approach, but the existing mechanism didn't work in a few situations because of the date sort. The update caused a re-render that caused an invalid unsortedFieldArrayMap.

Reproduction/how to test: 

**Updating bug was hit when you changed the date and it went before another date (re-ordering)**; which then modified the wrong date override. This PR replaces the entire array which retains the correct sort order by brute force.

**Delete bug was encountered if the sorted order was not the same as the filtered order, which depends on the order you created the date overrides in**. This PR implements an original order before the sort causing the right item to be removed.

Fixes #8043
2023-07-12 17:45:30 -07:00
Alex van Andel 605f7d35ec fix: Ensure rendering as UTC to prevent browser tz from messing up times
## What does this PR do?

Fixes all date shifts that are due browser time, by ensuring everything is UTC so it's not transformed to different times.

The times weren't actually shifting, dates were, but after casting these to UTC properly the times were also shifting; so that was a bug after a bugfix, time wise it was only a problem to determine all day events (which didn't completely work)

### Yes, but how do I test this?

Before:
Create a all day date override in +05:30
Shift your local system time to -04:00
Note that the day is different

After - do the same:
The day is the same!

Fixes #8365
Fixes #6978
Fixes #6482
2023-07-12 17:32:11 -07:00
Omar López 9b37d65503 fix: team invite links (#10110) 2023-07-12 14:24:47 -07:00
alannnc a1f9012501 fix: hasTeamPlan with accepted prop (#10109) 2023-07-12 16:40:41 -04:00
Ritik KumarandLeo Giovanetti 008016902e fix: incomplete vercel api error message on org creation (#9875)
* fix incomplete vercel api error message on org creation

* remove specified type from vercelCreateDomain

* have common error message for org domain registration

---------

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-07-12 20:29:13 +00:00
Crowdin Bot 9f33d85903 New Crowdin translations by Github Action 2023-07-12 20:22:52 +00:00
alannnc 496b03e92f fix: onboard-pricing-options-self-host (#10105) 2023-07-12 20:20:03 +00:00
Keith Williams 358da42a9b fix: yaml syntax for new contributors action (#10094) 2023-07-12 19:17:40 +00:00
Carina WollendorferandCarinaWolli 8812580f72 fix: slot reservation uses wrong event length (#10103)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-07-12 12:06:15 -07:00
Alex van Andel 3009f17cb0 fix: Use user locale for sending password resets (#10104) 2023-07-12 12:04:07 -07:00
Crowdin Bot 3328cb2226 New Crowdin translations by Github Action 2023-07-12 17:30:09 +00:00
Alex van Andel 7b57b4bcda fix: Password reset tests & better expiry checking (#10102) 2023-07-12 10:27:41 -07:00
Peer Richelsen 6d413f5721 fix: ternary operator with lenght causing 0 (#10090) 2023-07-12 12:17:30 +00:00
Crowdin Bot c9fc88ddc7 New Crowdin translations by Github Action 2023-07-12 10:17:03 +00:00
6d284ae15c fix: Allow editing guests when rescheduling or editing a booking (#9878)
* Allow editing guests when rescheduling or editing

* lint fix

* modify reschedule logic that assigned originalRescheduledBooking.attendees to the "rescheduled" booking to ensure that guests added or removed are used for the "rescheduled" booking

---------

Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-07-12 15:44:11 +05:30
Peer Richelsen c6791a8d3c fix: unnecessary whitespace in profile (#10071) 2023-07-11 21:26:28 +00:00
Leo Giovanetti aa1370b9c7 Enabling console for staging (#10072) 2023-07-11 21:21:30 +00:00
greatmaxy db27c7e4bc fix: Empty spaces in text field do not trigger inline field error (#10038) 2023-07-11 16:44:10 -04:00
Crowdin Bot 325c6ddc86 New Crowdin translations by Github Action 2023-07-11 20:28:56 +00:00
Udit TakkarandShivam Kalra 9c7428571d feat: added organizer daily video recording email (#9667)
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-07-11 16:26:30 -04:00
Crowdin Bot 9391bb5b64 New Crowdin translations by Github Action
release.yml / release (push) Canceled after 0s
2023-07-11 15:51:22 +00:00
d58924ecad feat: New workflow action to send Whatsapp message (#8818)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-07-11 15:48:44 +00:00
Hariom Balhara fdef15712a fix: Org booking pages link fixes for Routing Forms and Event type (#10052)
* Fix links

* Fix team event for org URL

* Fix avatar and user link
2023-07-11 17:46:13 +02:00
Joe Au-Yeung a5b5382306 fix: seats regression [CAL-2041]
## What does this PR do?

<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->

- Passes the proper seats data in the new booker component between states and to the backend

Fixes #9779
Fixes #9749
Fixes #7967 
Fixes #9942 

<!-- Please provide a loom video for visual changes to speed up reviews
 Loom Video: https://www.loom.com/
-->

## Type of change

<!-- Please delete bullets that are not relevant. -->

- Bug fix (non-breaking change which fixes an issue)

## How should this be tested?

<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->

**As the organizer** 
- Create a seated event type
- Book at least 2 seats
- Reschedule the booking
    - All attendees should be moved to the new booking
- Cancel the booking
    - The event should be cancelled for all attendees

**As an attendee**
- [x] Book a seated event
- [x] Reschedule that booking to an empty slot
    - [x] The attendee should be moved to that new slot
- [x] Reschedule onto a booking with occupied seats
    - [x] The attendees should be merged
- [x] On that slot reschedule all attendees to a new slot
    - [x] The former booking should be deleted
- [x] As the attendee cancel the booking
    - [x] Only that attendee should be removed 

## Mandatory Tasks

- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.

## Checklist

<!-- Please remove all the irrelevant bullets to your PR -->
2023-07-11 20:41:08 +05:30
Hariom Balhara c2fceb2e0f fix: avatars not loading for org events (#10062) 2023-07-11 14:23:07 +00:00
Crowdin Bot a73065ba5d New Crowdin translations by Github Action 2023-07-11 14:22:36 +00:00
Alex van Andel 7b0f677bdb Revert "fix: TS error in main (#10063)"
This reverts commit da80a2bae8.
2023-07-11 16:19:59 +02:00
Peer Richelsen 4166a0ab02 Update package.json 2023-07-11 15:47:46 +02:00
Hariom BalharaandKeith Williams da80a2bae8 fix: TS error in main (#10063)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-11 15:32:52 +02:00
Leo GiovanettiandPeer Richelsen c6fd91d181 fix: 404 links issue (#9928)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-11 13:05:41 +00:00
f62e6d9586 feat: enable crowdin 2-way sync (#9353)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-07-11 13:01:24 +00:00
Crowdin Bot b03c6d5040 New Crowdin translations by Github Action 2023-07-11 12:58:36 +00:00
Jeroen ReumkensandPeer Richelsen 172d77dcc3 feat: Added back to today button for new booker week and column views (#9684)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-11 12:57:48 +00:00
Syed Ali Shahbaz 1a905aa330 Add team example to POST (#10041) 2023-07-11 13:40:41 +02:00
Alex van Andel a8da4b87f0 Meticulous experiment & audit (#10050) 2023-07-11 13:37:59 +02:00
Crowdin Bot eb679a8e7d New Crowdin translations by Github Action 2023-07-11 11:32:50 +00:00
b364a85ed8 chore: removed old booker and make new booker as a default (#10053)
* removed old booker and make new booker as a default

* fixes merge conflict

* fixed tests

* fixed tests for old-booker

* fixed typo in @calcom/lib/defaultEvents.ts

---------

Co-authored-by: René Müller <rene.mueller@clicksports.de>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-11 13:31:55 +02:00
Omar López a2e0d44fb8 fix: prevents warnings on branding call when unauthed (#10051) 2023-07-11 12:06:44 +02:00
Mehul a9ee5d6f7f fixes: about section unnecessary scrollbar (#10043) 2023-07-11 11:26:43 +02:00
sean-brydonandLeo Giovanetti 6d35750784 feat: Auto accept on org verify (#10037)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-07-11 10:30:45 +02:00
Anik Dhabal BabuandLeo Giovanetti 4f423ba692 fix: Reject Organization button is not working (#10010)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-07-11 10:27:53 +02:00
Pradumn Kumar d097c1fada fix: fixes issue with webhook not being triggered for managed events (#10025) 2023-07-11 13:16:24 +05:30
Crowdin Bot fc049e12b3 New Crowdin translations by Github Action 2023-07-11 07:42:07 +00:00
Joe Au-YeungandLeo Giovanetti 267422a1f4 fix: Enable Salesforce & CRMs for teams & orgs [CAL-2116] (#9973)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-07-11 13:11:21 +05:30
Crowdin Bot 6e15de31aa New Crowdin translations by Github Action 2023-07-10 22:33:08 +00:00
Carina Wollendorfer 2f6b5ced04 fix: better slot starting times
## What does this PR do?

Currently, we start the first slot always at the nearest 15 minutes. This is not ideal as for some duration other slot starting time make more sense. So with this PR the starting times are defined as follow: 

- Frequency is exact hours (60, 120, 180, ...), slot start time is a full hour
- Frequency is half hours (30, 90, ...), slot start time is half or full hours (8:00, 8:30, ...)
- Same with 20-minute events (20, 40, ...) and 10-minute events 
- Everything else will start at the nearest 15 min slot

It also fixes that slot times are shifted when there is a busy slot with a different duration. Here is a before and after of a 30-min event with a 5-minute busy slot at 1:00 pm

Before: 
![Screenshot 2023-07-07 at 13 31 45](https://github.com/calcom/cal.com/assets/30310907/b92d4ff4-49f1-48f4-a973-99266f61d919)

After
![Screenshot 2023-07-07 at 13 34 01](https://github.com/calcom/cal.com/assets/30310907/042c7ef7-8c2a-4cd9-b663-183bc07b5864)


#### 30 Minute events, availability starting at 7:15

Before: 
![Screenshot 2023-07-06 at 12 40 00](https://github.com/calcom/cal.com/assets/30310907/752ed978-83cf-4ee9-a38d-b5795df6daec)

After:
![Screenshot 2023-07-06 at 12 40 42](https://github.com/calcom/cal.com/assets/30310907/5d51ec15-5be8-4f3b-b374-46dad35216b8)

## Type of change
- Bug fix (non-breaking change which fixes an issue)

## How should this be tested?

- Check if slot times are shown as described
- Test with different intervals/durations
- Test with busy times
- Test with different availabilities 

## Mandatory Tasks

- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
2023-07-10 15:32:26 -07:00
Omar López cf57be2aef fix: Calcom UI deployments (#10047) 2023-07-10 19:52:15 +00:00
Hariom Balhara c2996cd2f8 fix: 404 booking pages when org support is enabled locally
## What does this PR do?
Fix 404 booking pages when org support is enabled locally

## Type of change

<!-- Please delete bullets that are not relevant. -->

- Bug fix (non-breaking change which fixes an issue)

## How should this be tested?

<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->

-  Add localhost:3000 to ALLOWED_HOSTNAMES which is the default recommendation for enabling org support locally.
<img width="1035" alt="Screenshot 2023-07-10 at 11 04 35 AM" src="https://github.com/calcom/cal.com/assets/1780212/7272437b-afae-4073-905b-eafc39f9fafc">
- Simply restart the server and visit http://localhost:3000/pro. It will give 404. After this change it would work

## Mandatory Tasks

- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
2023-07-10 12:19:33 -07:00
Crowdin Bot 79944dfed1 New Crowdin translations by Github Action 2023-07-10 19:14:15 +00:00
sean-brydonandzomars e83647c652 feat: Add password checks for password-reset (#10042)
Co-authored-by: zomars <zomars@me.com>
2023-07-10 12:13:20 -07:00
Crowdin Bot 556bf346f0 New Crowdin translations by Github Action 2023-07-10 14:45:23 +00:00
Leo Giovanetti b98f2045d0 chore: Teams empty screen (#9997) 2023-07-10 10:44:22 -04:00
Eric DantasandPeer Richelsen a13c050e9d chore: Improve info that should be filled out for bugs and features (#9978)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-10 16:17:52 +02:00
Ujwal Kumar e088196c8d fix: vertical-scrollbar-issue-when-organization-table-contains-no-records (#10008) 2023-07-10 16:16:20 +02:00
Leo Giovanetti 9456ffbb48 chore: Credits with commit hash (#9999) 2023-07-10 16:10:30 +02:00
Sai Krishna 6d19cc22a5 fix: cancel button not working on payment page (#10006) 2023-07-10 15:58:38 +02:00
Akshay c69c9d6314 Fix #10021 Changes save button color from secondary to primary. (#10022) 2023-07-10 15:30:00 +02:00
e0ef30eb52 fix: /routing-form/single - Don't clear edits when moving from one tab to the other (#8125)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-07-10 09:24:04 -04:00
Vinoth Kumar VandVinoth Kumar V 045828d4ab fix: location icon (#9994)
Co-authored-by: Vinoth Kumar V <vinoth_kumar_v@Vinoths-MacBook-Pro.local>
2023-07-10 12:57:34 +02:00
Hariom Balhara 354d57f393 Add border in embed always (#10034) 2023-07-10 09:56:41 +00:00
650d0082a3 feat: from org team invite (#9569)
* Initial commit

* Adding feature flag

* Desktop first banner, mobile pending

* Removing dead code and img

* AppInstallButtonBase

* WIP

* Adds Email verification template+translations for organizations (#9202)

* feat: Orgs Schema Changing `scopedMembers` to `orgUsers` (#9209)

* Change scopedMembers to orgMembers

* Change to orgUsers

* First step done

* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding

* Session logic to show org label

* Step 2 done, avatar not working

* List orgs and list teams specific if orgs exist

* Conditionally show org - fix settings layout - add labels for all pages

* Profile Page + update

* Org specific team creation

* appearance page

* Ensure members cant of org cant update settings in UI

* Fix update handler imports

* hide billing on sub teams

* Update profile slug page

* Letting duplicate slugs for teams to support orgs

* Add slug coliisions for org

* Covering null on unique clauses

* Covering null on unique clauses

* Extract to utils

* Update settings to use subdomain path in team url , team + org

* Supporting having the orgId in the session cookie

* Onboarding admins step

* Last step to create teams

* Update handler comments

* Upgrade ORG banner - disabled team banner for child teams

* Handle publishing ORGS

* Fix licenese issue

* Update packages/trpc/server/routers/viewer/teams/create.handler.ts

* Split into function calls to make this file more explisit

* Update parents stripe sub not teamID

* Moving change password handler, improving verifying code flow

* Clearing error before submitting

* Reverting email testing api changes

* Reverting having the banner for now

* Consistent exported components

* Remove unneeded files from banner

* Removing uneeded code

* Fixing avatar selector

* Using meta component for head/descr

* Missing i18n strings

* Create org membership also - billing portal page

* A11ly

* Hide create team if no valid permisisons

* Get Org members router

* Handle updating subscription if orgId

* Fix double upgrade banner

* Update constants

* Feedback

* Copy change

* Making an org avatar (temp)

* Add slug colission detection for user and team name

* Fix Import

* Remove update password func

* Fix module import over relative

* feat: organization event type filter (#9253)

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Missing changes to support orgs schema changes

* Fix import again

* Throw no team found before auth error

* Check if invited found user is already in differnt org

* feat: organization settings general page

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* feat: add members page

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore: remove

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: use invalidate

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Move to for of loop to throw errors in usenamelist

* fix: delete mutation

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: remove organization id

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Remove app install button sa its in 9337

* Remove i18n key not being used

* feat: Onboarding process to create an organization (#9184)

* Desktop first banner, mobile pending

* Removing dead code and img

* WIP

* Adds Email verification template+translations for organizations (#9202)

* First step done

* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding

* Step 2 done, avatar not working

* Covering null on unique clauses

* Onboarding admins step

* Last step to create teams

* Moving change password handler, improving verifying code flow

* Clearing error before submitting

* Reverting email testing api changes

* Reverting having the banner for now

* Consistent exported components

* Remove unneeded files from banner

* Removing uneeded code

* Fixing avatar selector

* Using meta component for head/descr

* Missing i18n strings

* Feedback

* Making an org avatar (temp)

* Check for subteams slug clashes with usernames

* Fixing create teams onsuccess

* feedback

* Making sure we check requestedSlug now

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

* feat: [CAL-1816] Organization subdomain support (#9345)

* Desktop first banner, mobile pending

* Removing dead code and img

* WIP

* Adds Email verification template+translations for organizations (#9202)

* First step done

* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding

* Step 2 done, avatar not working

* Covering null on unique clauses

* Onboarding admins step

* Last step to create teams

* Moving change password handler, improving verifying code flow

* Clearing error before submitting

* Reverting email testing api changes

* Reverting having the banner for now

* Consistent exported components

* Remove unneeded files from banner

* Removing uneeded code

* Fixing avatar selector

* Using meta component for head/descr

* Missing i18n strings

* Feedback

* Making an org avatar (temp)

* Check for subteams slug clashes with usernames

* Fixing create teams onsuccess

* Covering users and subteams, excluding non-org users

* Unpublished teams shows correctly

* Create subdomain in Vercel

* feedback

* Renaming Vercel env vars

* Vercel domain check before creation

* Supporting cal-staging.com

* Change to have vercel detect it

* vercel domain check data message error

* Remove check domain

* Making sure we check requestedSlug now

* Feedback and unneeded code

* Reverting unneeded changes

* Unneeded changes

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

* Vercel subdomain creation in PROD only

* Fix router

* fix: use zod schema

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* feat: organization settings general and members page (#9266)

* feat: organization settings general page

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* feat: add members page

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore: remove

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: use invalidate

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: delete mutation

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: remove organization id

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: use zod schema

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Type fixes

* Use org Stripe product when upgrading

* Removed unused code

* UI completed

* Reverting changes

* Update UsernameTextfield.tsx

* More reverts

* Update next-auth-options.ts

* Update common.json

* Type fixes

* Include invite token for orgs

* Update org schema

* Make token settings optional as it isnt used in orgs yet

* Reverts

* Add correct array logic and move to controlled component

* Fix toggle group default | Update toggle group bg

* Darkmode toggle group

* Distinct user

* Hide modal if no org members

* Extract toggle logic

* Update packages/features/ee/organizations/components/TeamInviteFromOrg.tsx

* remove yarn.lock from commit

* Fix types

* Add getMember router back

* As a query lol

* Fix types

* Fix accepted param defaulting to true as we want both

* Fix list not pulling back people who have not joined the org yet

* Fix tests to handle invite a existing org user to a team

* Fix test

* Fix an error sometimes when existing org user is invited

* Updates radix & fixes bug

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-07-10 15:16:51 +05:30
Crowdin Bot 7c4b7209c2 New Crowdin translations by Github Action 2023-07-07 23:58:52 +00:00
sydwardrae 5736821466 fixed typo string to strong (#10001) 2023-07-07 16:58:01 -07:00
Joe Au-Yeungandzomars 75f76c130a fix: Orgs/create child teams CAL-1986 (#9631)
* Re-applied valid changes

* Update packages/trpc/server/middlewares/sessionMiddleware.ts

* Type fix

* Type fix

---------

Co-authored-by: zomars <zomars@me.com>
2023-07-07 16:48:51 +00:00
Anik Dhabal Babu 936432a730 fix: Back to Bookings button only shows for organizer (#9983) 2023-07-07 11:50:44 -04:00
Hariom Balharaandalannnc 4fda760e5e fix: Organizations/ Fixes related to Routing Form, Booking Pages and Embeds (#9860)
* Fix Team Events not working

* Get embed for org events working

* Fix rewrites

* Update next-config.test.ts

Spacing changes

* Update next-config.test.ts

Scan the file through prettier and update

---------

Co-authored-by: alannnc <alannnc@gmail.com>
2023-07-07 21:20:15 +05:30
Ritik Kumar c467fd41ac fix: hide invite member button from list tile for team members (#9963) 2023-07-07 09:34:57 -04:00
sean-brydon 0969d5960a feat: temp admin user management
## What does this PR do?
Loom: https://www.loom.com/share/361ca6ca9b5748079cba59ed2f348d90

Not all items on edit from update the user. This is very temp page to help Milos

How to test:
Login as admin user
settings > admin > users
search - see filtered users
scroll to bottom of list - notice we refetch before you hit the bottom so you dont notice items loading
delete user -> user is deleted and removed from list (radix bug in main causes dialog to freeze refresh to make this go away)
edit user -> Edit users email/username/identityprovider. Not all items on this update work just yet. These will be picked up when we come to implement the proper user table.
2023-07-07 18:25:21 +05:30
Alex van Andel cbe1907118 Only enable when defaultScheduleId is truthy (#9968) 2023-07-06 17:06:37 -07:00
Alex van Andel afd41475c9 fix: update should be based on email only (#9977) 2023-07-06 23:55:00 +00:00
533 changed files with 17576 additions and 8063 deletions
+1 -6
View File
@@ -121,6 +121,7 @@ TWILIO_SID=
TWILIO_TOKEN=
TWILIO_MESSAGING_SID=
TWILIO_PHONE_NUMBER=
TWILIO_WHATSAPP_PHONE_NUMBER=
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
NEXT_PUBLIC_SENDER_ID=
TWILIO_VERIFY_SID=
@@ -214,9 +215,3 @@ PROJECT_ID_VERCEL=
TEAM_ID_VERCEL=
# Get it from: https://vercel.com/account/tokens
AUTH_BEARER_TOKEN_VERCEL=
#Enables New booker for Embed only
NEW_BOOKER_ENABLED_FOR_EMBED=0
#Enables New booker for All but Embed requests
NEW_BOOKER_ENABLED_FOR_NON_EMBED=0
+12
View File
@@ -20,8 +20,20 @@ A summary of the issue. This needs to be a clear detailed-rich summary.
Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead?
### Actual Results
- What's happening right now that is different from what is expected
### Expected Results
- This is an ideal result that the system should get after the tests are performed
### Technical details
- Browser version, screen recording, console logs, network requests: You can make a recording with [Bird Eats Bug](https://birdeatsbug.com/).
- Node.js version
- Anything else that you think could be an issue.
### Evidence
- How was this tested? This is quite mandatory in terms of bugs. Providing evidence of your testing with screenshots or/and videos is an amazing way to prove the bug and a troubleshooting chance to find the solution.
@@ -39,3 +39,11 @@ assignees: ""
-->
(Write your answer here.)
### Requirement/Document
<!--
Is there any type of document that could support that feature?
-->
(Share it here.)
+17 -8
View File
@@ -8,22 +8,31 @@ Fixes # (issue)
Loom Video: https://www.loom.com/
-->
## Requirement/Documentation
<!-- Please provide all documents that are important to understand the reason of that PR. -->
- If there is a requirement document, please, share it here.
- If there is ab UI/UX design document, please, share it here.
## Type of change
<!-- Please delete bullets that are not relevant. -->
- Bug fix (non-breaking change which fixes an issue)
- Chore (refactoring code, technical debt, workflow improvements)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
- This change requires a documentation update
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
## How should this be tested?
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests -->
- [ ] Test A
- [ ] Test B
- Are there environment variables that should be set?
- What are the minimal test data to have?
- What is expected (happy path) to have (input and output)?
- Any other important info that could help to test that PR
## Mandatory Tasks
+6 -4
View File
@@ -2,14 +2,16 @@ name: "Set environment variables"
description: "Configures environment variables for a workflow"
runs:
using: "composite"
steps:
- name: Create env file
- name: Read env file
uses: buildjet/cache@v3
id: env-cache
with:
path: gh.env
key: env-cache-${{ hashFiles('gh.env') }}
path: gh.env-${{ github.sha }}
key: env-cache
restore-keys: env-cache-
- name: Set Environment Variables
uses: tw3lveparsecs/github-actions-setvars@latest
with:
envFilePath: gh.env
envFilePath: gh.env-${{ github.sha }}
@@ -0,0 +1,23 @@
name: Cron - scheduleWhatsappReminders
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
jobs:
cron-scheduleWhatsappReminders:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_API_KEY }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/workflows/scheduleWhatsappReminders \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
+1
View File
@@ -22,6 +22,7 @@ jobs:
uses: crowdin/github-action@1.5.1
with:
upload_sources: true
upload_translations: true
download_translations: true
push_translations: true
commit_message: "New Crowdin translations by Github Action"
+9 -4
View File
@@ -5,7 +5,7 @@ on:
jobs:
e2e-app-store:
timeout-minutes: 20
name: E2E App-Store Apps
name: E2E App Store (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: buildjet-4vcpu-ubuntu-2204
services:
postgres:
@@ -15,6 +15,11 @@ jobs:
POSTGRES_DB: calendso
ports:
- 5432:5432
strategy:
fail-fast: false
matrix:
## There aren't many tests for AppStore. So, just start with 2 shards. Increase if needed.
shard: [1, 2]
steps:
- uses: actions/checkout@v3
@@ -26,12 +31,12 @@ jobs:
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn test-e2e:app-store
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results
path: test-results
name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: app-store-results
+10 -4
View File
@@ -1,10 +1,11 @@
name: E2E Embed tests and booking flow(for non-embed as well)
name: E2E Embed React tests and booking flow(for non-embed as well)
on:
workflow_call:
jobs:
e2e-embed:
timeout-minutes: 20
name: E2E Embed React (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: buildjet-4vcpu-ubuntu-2204
services:
postgres:
@@ -14,6 +15,11 @@ jobs:
POSTGRES_DB: calendso
ports:
- 5432:5432
strategy:
fail-fast: false
matrix:
## There aren't many tests for embed-react. So, just start with 2 shards. Increase if needed.
shard: [1, 2]
steps:
- uses: actions/checkout@v3
@@ -26,7 +32,7 @@ jobs:
- uses: ./.github/actions/cache-build
- name: Run Tests
run: |
yarn test-e2e:embed-react
yarn e2e:embed-react --shard=${{ matrix.shard }}/${{ strategy.job-total }}
yarn workspace @calcom/embed-react packaged:tests
env:
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
@@ -34,5 +40,5 @@ jobs:
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results
path: test-results
name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: embed-react-results
+10 -4
View File
@@ -1,10 +1,11 @@
name: E2E Embed tests and booking flow(for non-embed as well)
name: E2E Embed Core tests and booking flow(for non-embed as well)
on:
workflow_call:
jobs:
e2e-embed:
timeout-minutes: 20
name: E2E Embed Core (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: buildjet-4vcpu-ubuntu-2204
services:
postgres:
@@ -14,6 +15,11 @@ jobs:
POSTGRES_DB: calendso
ports:
- 5432:5432
strategy:
fail-fast: false
matrix:
## There aren't many tests for embed-core. So, just start with 2 shards. Increase if needed.
shard: [1, 2]
steps:
- uses: actions/checkout@v3
@@ -25,12 +31,12 @@ jobs:
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn test-e2e:embed
run: yarn e2e:embed --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results
path: test-results
name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: embed-core-results
+5 -3
View File
@@ -4,6 +4,7 @@ on:
workflow_call:
env:
ENV_FILE_NAME: gh.env-${{ github.sha }}
INPUT_ENV_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
INPUT_ENV_NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
INPUT_ENV_NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
@@ -44,10 +45,11 @@ jobs:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ozaytsev86/create-env-file@v1
with:
file-name: ${{ github.workspace }}/gh.env
file-name: ${{ github.workspace }}/${{ env.ENV_FILE_NAME }}
- uses: buildjet/cache@v3
id: env-cache
with:
path: gh.env
key: env-cache-${{ hashFiles('gh.env') }}
path: ${{ env.ENV_FILE_NAME }}
key: env-cache-${{ hashFiles(env.ENV_FILE_NAME) }}
@@ -7,8 +7,13 @@ on:
- main
jobs:
env:
name: Create env file
uses: ./.github/workflows/env-create-file.yml
secrets: inherit
build:
name: Production build
needs: env
if: ${{ github.event_name == 'push' }}
uses: ./.github/workflows/production-build.yml
secrets: inherit
+1 -4
View File
@@ -84,21 +84,18 @@ jobs:
e2e-app-store:
name: E2E App Store tests
if: ${{ needs.changes.outputs.app-store == 'true' }}
needs: [changes, lint, build]
uses: ./.github/workflows/e2e-app-store.yml
secrets: inherit
e2e-embed:
name: E2E embeds tests
if: ${{ needs.changes.outputs.embed == 'true' }}
needs: [changes, lint, build]
uses: ./.github/workflows/e2e-embed.yml
secrets: inherit
e2e-embed-react:
name: E2E React embeds tests
if: ${{ needs.changes.outputs.embed-react == 'true' }}
needs: [changes, lint, build]
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
@@ -109,7 +106,7 @@ jobs:
secrets: inherit
required:
needs: [lint, type-check, test, build, e2e]
needs: [lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
if: always()
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
+47
View File
@@ -0,0 +1,47 @@
name: "Release Docker"
on: # yamllint disable-line rule:truthy
release:
types:
- created
# in case manual trigger is needed
workflow_dispatch:
inputs:
RELEASE_TAG:
description: 'v{Major}.{Minor}.{Patch}'
jobs:
release:
name: "Remote Release"
runs-on: "ubuntu-latest"
steps:
- name: checkout
uses: actions/checkout@v3
- name: "Determine tag"
run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV"
- name: "Run remote release workflow"
uses: "actions/github-script@v6"
with:
# Requires a personal access token with Actions Read and write permissions on calcom/docker.
github-token: "${{ secrets.DOCKER_REPO_ACCESS_TOKEN }}"
script: |
try {
const response = await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: 'docker',
workflow_id: 'create-release.yaml',
ref: 'main',
inputs: {
"RELEASE_TAG": process.env.RELEASE_TAG
},
});
console.log(response);
} catch (error) {
console.error(error);
core.setFailed(error.message);
}
+15 -15
View File
@@ -1,7 +1,7 @@
name: "Welcome new contributors"
on:
issues:
on:
issues:
types: opened
pull_request:
types: opened
@@ -13,16 +13,16 @@ permissions:
jobs:
welcome-message:
name: Welcoming New Users
runs-on: ubuntu-latest
timeout-minutes: 10
needs: semantics
if: github.event.action == 'opened'
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: |-
Thank you for making your first Pull Request and taking the time to improve Cal.com ! ❤️🎉
Feel free to join the conversation at [discord](https://go.cal.com/discord)
issue-message: |
Thank you for opening your first issue, one of our team members will review it as soon as it possible. ❤️🎉
runs-on: ubuntu-latest
timeout-minutes: 10
needs: semantics
if: github.event.action == 'opened'
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: |-
Thank you for making your first Pull Request and taking the time to improve Cal.com ! ❤️🎉
Feel free to join the conversation at [discord](https://go.cal.com/discord)
issue-message: |
Thank you for opening your first issue, one of our team members will review it as soon as it possible. ❤️🎉
-1
View File
@@ -90,7 +90,6 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
- [Prisma.io](https://prisma.io/?ref=cal.com)
- [Daily.co](https://go.cal.com/daily)
## Contact us
Meet our sales team for any commercial inquiries.
+1 -1
View File
@@ -7,7 +7,7 @@
<a href="https://cal.com/docs/enterprise-features/api#api-server-specifications">Read the API docs</a>
</div>
# Commercial Cal.com Public API
# Commercial Cal.com Public API
Welcome to the Public API ("/apps/api") of the Cal.com.
+6 -2
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import slugify from "@calcom/lib/slugify";
import { _EventTypeModel as EventType, _HostModel } from "@calcom/prisma/zod";
import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
@@ -58,7 +59,7 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
const schemaEventTypeCreateParams = z
.object({
title: z.string(),
slug: z.string(),
slug: z.string().transform((s) => slugify(s)),
description: z.string().optional().nullable(),
length: z.number().int(),
metadata: z.any().optional(),
@@ -76,7 +77,10 @@ export const schemaEventTypeCreateBodyParams = schemaEventTypeBaseBodyParams
const schemaEventTypeEditParams = z
.object({
title: z.string().optional(),
slug: z.string().optional(),
slug: z
.string()
.transform((s) => slugify(s))
.optional(),
length: z.number().int().optional(),
seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(),
+8 -1
View File
@@ -57,7 +57,14 @@ import { defaultResponder } from "@calcom/lib/server";
* format: email
* description: 'Attendee email address'
* location:
* type: string
* type: object
* properties:
* optionValue:
* type: string
* description: 'Option value for the location'
* value:
* type: string
* description: 'The meeting URL, Phone number or Address'
* description: 'Meeting location'
* metadata:
* type: object
+38 -3
View File
@@ -47,7 +47,6 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
* slug:
* type: string
* description: Unique slug for the event type
* example: my-event
* hosts:
* type: array
* items:
@@ -168,9 +167,9 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
* type: string
* displayLocationPublicly:
* type: boolean
* example:
* examples:
* event-type:
* summary: An example of event type POST request
* summary: An example of an individual event type POST request
* value:
* title: Hello World
* slug: hello-world
@@ -205,6 +204,42 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
* }
* }
* }
* team-event-type:
* summary: An example of a team event type POST request
* value:
* title: "Tennis class"
* slug: "tennis-class-{{$guid}}"
* length: 60
* hidden: false
* position: 0
* teamId: 3
* eventName: null
* timeZone: null
* periodType: "UNLIMITED"
* periodStartDate: null
* periodEndDate: null
* periodDays: null
* periodCountCalendarDays: null
* requiresConfirmation: true
* recurringEvent:
* interval: 2
* count: 10
* freq: 2
* disableGuests: false
* hideCalendarNotes: false
* minimumBookingNotice: 120
* beforeEventBuffer: 0
* afterEventBuffer: 0
* schedulingType: "COLLECTIVE"
* price: 0
* currency: "usd"
* slotInterval: null
* successRedirectUrl: null
* description: null
* locations:
* - address: "London"
* type: "inPerson"
* metadata: {}
* tags:
* - event-types
* externalDocs:
+1 -1
View File
@@ -12,7 +12,7 @@
"@calcom/ui": "*",
"@radix-ui/react-avatar": "^1.0.0",
"@radix-ui/react-collapsible": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-id": "^1.0.0",
"@radix-ui/react-popover": "^1.0.2",
+34
View File
@@ -1,5 +1,39 @@
# @calcom/web
## 3.1.3
### Patch Changes
- Updated dependencies
- @calcom/embed-react@1.3.0
## 3.0.10
### Patch Changes
- Updated dependencies
- @calcom/embed-snippet@1.1.2
- @calcom/embed-react@1.2.2
- @calcom/embed-core@1.3.2
## 3.0.9
### Patch Changes
- Updated dependencies
- @calcom/embed-snippet@1.1.1
- @calcom/embed-react@1.2.1
- @calcom/embed-core@1.3.1
## 3.0.8
### Patch Changes
- Updated dependencies
- @calcom/embed-core@1.3.0
- @calcom/embed-react@1.2.0
- @calcom/embed-snippet@1.1.0
## 2.9.4
### Patch Changes
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -1,4 +1,3 @@
import { isEmpty } from "lodash";
import { useTranslation } from "next-i18next";
import { useEffect } from "react";
@@ -20,13 +19,13 @@ export function useViewerI18n() {
/**
* Auto-switches locale client-side to the logged in user's preference
*/
const I18nLanguageHandler = (): null => {
const I18nLanguageHandler = () => {
const { i18n } = useTranslation("common");
const locale = useViewerI18n().data?.locale || i18n.language;
useEffect(() => {
// bail early when i18n = {}
if (isEmpty(i18n)) return;
if (Object.keys(i18n).length === 0) return;
// if locale is ready and the i18n.language does != locale - changeLanguage
if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale);
@@ -35,7 +34,6 @@ const I18nLanguageHandler = (): null => {
document.dir = i18n.dir();
document.documentElement.setAttribute("lang", locale);
}, [locale, i18n]);
return null;
};
+2 -6
View File
@@ -6,7 +6,7 @@ import Script from "next/script";
import "@calcom/embed-core/src/embed-iframe";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { WEBAPP_URL, IS_CALCOM } from "@calcom/lib/constants";
import { buildCanonical } from "@calcom/lib/next-seo.config";
import type { AppProps } from "@lib/app-providers";
@@ -53,10 +53,6 @@ function PageWrapper(props: AppProps) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page);
// Canonical: Check if the URL is from cal.com so that we set the canonical conditionally
const isCalcom =
WEBAPP_URL &&
(new URL(WEBAPP_URL).hostname.endsWith("cal.com") || new URL(WEBAPP_URL).hostname.endsWith("cal.dev"));
const path = router.asPath;
return (
@@ -70,7 +66,7 @@ function PageWrapper(props: AppProps) {
<DefaultSeo
// Set canonical to https://cal.com or self-hosted URL
canonical={
isCalcom
IS_CALCOM
? buildCanonical({ path, origin: "https://cal.com" }) // cal.com & .dev
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
}
+46 -34
View File
@@ -3,7 +3,7 @@ import type { IframeHTMLAttributes } from "react";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/router";
import { CAL_URL } from "@calcom/lib/constants";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
@@ -29,6 +29,8 @@ import {
Avatar,
} from "@calcom/ui";
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon";
import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner";
/* These app slugs all require Google Cal to be installed */
@@ -215,7 +217,7 @@ const Component = ({
};
}
return (
<InstallAppButtonChild appCategories={categories} userAdminTeams={appDbQuery.data?.userAdminTeams} addAppMutationInput={{type, variant, slug}} credentials={appDbQuery.data?.credentials} {...props} />
<InstallAppButtonChild appCategories={categories} userAdminTeams={appDbQuery.data?.userAdminTeams} addAppMutationInput={{ type, variant, slug }} credentials={appDbQuery.data?.credentials} {...props} />
);
}}
/>
@@ -414,7 +416,7 @@ const InstallAppButtonChild = ({
},
});
if (!userAdminTeams?.length || appCategories.some((category) => category === "calendar")) {
if (!userAdminTeams?.length || appCategories.some((category) => ["calendar", "conferencing"].includes(category))) {
return <Button
data-testid="install-app-button"
{...props}
@@ -424,7 +426,7 @@ const InstallAppButtonChild = ({
size="base">
{multiInstall ? t("install_another") : t("install_app")}
</Button>
}
return (
@@ -441,41 +443,51 @@ const InstallAppButtonChild = ({
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent>
<DropdownMenuContent
onInteractOutside={(event) => {
if (mutation.isLoading) event.preventDefault();
}}
>
{mutation.isLoading && (
<div className="z-1 fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<Spinner />
</div>
)}
<DropdownMenuLabel>{t("install_app_on")}</DropdownMenuLabel>
{userAdminTeams.map((team) => {
const isInstalled = credentials &&
credentials.some((credential) =>
credential?.teamId ? credential?.teamId === team.id : credential.userId === team.id
)
credentials.some((credential) =>
credential?.teamId ? credential?.teamId === team.id : credential.userId === team.id
)
return (
<DropdownItem
type="button"
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
key={team.id}
disabled={
isInstalled
}
StartIcon={(props) => (
<Avatar
alt={team.logo || ""}
imageSrc={team.logo || `${CAL_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
size="sm"
{...props}
/>
)}
onClick={() => {
mutation.mutate(
team.isUser ? addAppMutationInput : { ...addAppMutationInput, teamId: team.id }
);
}}>
<p>{team.name}{" "}
{isInstalled &&
`(${t("installed")})`}</p>
</DropdownItem>
)})}
<DropdownItem
type="button"
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
key={team.id}
disabled={
isInstalled
}
StartIcon={(props) => (
<Avatar
alt={team.logo || ""}
imageSrc={team.logo || `${CAL_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
size="sm"
{...props}
/>
)}
onClick={() => {
mutation.mutate(
team.isUser ? addAppMutationInput : { ...addAppMutationInput, teamId: team.id }
);
}}>
<p>{team.name}{" "}
{isInstalled &&
`(${t("installed")})`}</p>
</DropdownItem>
)
})}
</DropdownMenuContent>
</DropdownMenuPortal>
</Dropdown>
@@ -1,51 +1,117 @@
import type {
DefaultEventLocationType,
EventLocationTypeFromApp,
LocationObject,
} from "@calcom/app-store/locations";
import { getEventLocationType, getTranslatedLocation } from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "@calcom/ui";
import { Link } from "@calcom/ui/components/icon";
import type { Props } from "./pages/AvailabilityPage";
const excludeNullValues = (value: unknown) => !!value;
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
function RenderIcon({
eventLocationType,
isTooltip,
}: {
eventLocationType: DefaultEventLocationType | EventLocationTypeFromApp;
isTooltip: boolean;
}) {
return (
<img
src={eventLocationType.iconUrl}
className={classNames(
"me-[10px] h-4 w-4 opacity-70 dark:opacity-100",
!eventLocationType.iconUrl?.startsWith("/app-store") ? "dark:invert-[.65]" : "",
!eventLocationType.iconUrl?.startsWith("/app-store") && isTooltip && "invert"
)}
alt={`${eventLocationType.label} icon`}
/>
);
}
function RenderLocationTooltip({ locations }: { locations: LocationObject[] }) {
const { t } = useLocale();
const renderLocations = locations.map((location, index) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
// It's possible that the location app got uninstalled
return null;
}
if (eventLocationType.variable === "hostDefault") {
return null;
}
return (
<div className="my-2 me-2 flex w-full flex-col space-y-3 break-words">
<p>{t("select_on_next_step")}</p>
{locations.map(
(
location: Pick<Partial<LocationObject>, "link" | "address"> &
Omit<LocationObject, "link" | "address">,
index: number
) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
return null;
}
const translatedLocation = getTranslatedLocation(location, eventLocationType, t);
return (
<div key={`${location.type}-${index}`} className="font-sm flex flex-row items-center">
<RenderIcon eventLocationType={eventLocationType} isTooltip />
<p className="line-clamp-1">{translatedLocation}</p>
</div>
);
}
)}
</div>
);
}
const translatedLocation = getTranslatedLocation(location, eventLocationType, t);
export function AvailableEventLocations({ locations }: { locations: LocationObject[] }) {
const { t } = useLocale();
return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? (
<Link className="text-default ml-[2px] h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
) : (
<img
src={eventLocationType.iconUrl}
className={classNames(
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
!eventLocationType.iconUrl?.startsWith("/app-store") ? "dark:invert-[.65]" : ""
)}
alt={`${eventLocationType.label} icon`}
/>
)}
<Tooltip content={translatedLocation}>
<p className="line-clamp-1">{translatedLocation}</p>
</Tooltip>
</div>
);
});
const renderLocations = locations.map(
(
location: Pick<Partial<LocationObject>, "link" | "address"> & Omit<LocationObject, "link" | "address">,
index: number
) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
// It's possible that the location app got uninstalled
return null;
}
if (eventLocationType.variable === "hostDefault") {
return null;
}
const translatedLocation = getTranslatedLocation(location, eventLocationType, t);
return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? (
<Link className="text-default ml-[2px] h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
) : (
<RenderIcon eventLocationType={eventLocationType} isTooltip={false} />
)}
<Tooltip content={translatedLocation}>
<p className="line-clamp-1">{translatedLocation}</p>
</Tooltip>
</div>
);
}
);
const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[];
return filteredLocations.length ? (
return filteredLocations.length > 1 ? (
<div className="flex flex-row items-center text-sm font-medium">
<img
src="/map-pin.svg"
className={classNames("me-[10px] h-4 w-4 opacity-70 dark:invert")}
alt="map-pin"
/>
<Tooltip content={<RenderLocationTooltip locations={locations} />}>
<p className="line-clamp-1">
{t("location_options", {
locationCount: filteredLocations.length,
})}
</p>
</Tooltip>
</div>
) : filteredLocations.length === 1 ? (
<div className="text-default mr-6 flex w-full flex-col space-y-4 break-words text-sm">
{filteredLocations}
</div>
+21 -10
View File
@@ -66,6 +66,10 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
);
const reserveSlot = (slot: Slot) => {
// Prevent double clicking
if (reserveSlotMutation.isLoading || reserveSlotMutation.isSuccess) {
return;
}
reserveSlotMutation.mutate({
slotUtcStartDate: slot.time,
eventTypeId,
@@ -132,8 +136,20 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
let slotFull, notEnoughSeats;
if (slot.attendees && seatsPerTimeSlot) slotFull = slot.attendees >= seatsPerTimeSlot;
if (slot.attendees && bookingAttendees && seatsPerTimeSlot)
if (slot.attendees && bookingAttendees && seatsPerTimeSlot) {
notEnoughSeats = slot.attendees + bookingAttendees > seatsPerTimeSlot;
}
const isHalfFull =
slot.attendees && seatsPerTimeSlot && slot.attendees / seatsPerTimeSlot >= 0.5;
const isNearlyFull =
slot.attendees && seatsPerTimeSlot && slot.attendees / seatsPerTimeSlot >= 0.83;
const colorClass = isNearlyFull
? "text-rose-600"
: isHalfFull
? "text-yellow-500"
: "text-emerald-400";
return (
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
@@ -144,7 +160,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
className={classNames(
"text-default bg-default border-subtle mb-2 block rounded-sm border py-2 font-medium opacity-25",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}>
)}
data-testid="time"
data-disabled="true">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{notEnoughSeats ? (
<p className="text-sm">{t("not_enough_seats")}</p>
@@ -165,14 +183,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
data-disabled="false">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && (
<p
className={`${
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
? "text-rose-600"
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-emerald-400"
} text-sm`}>
<p className={`${colorClass} text-sm`}>
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
{seatsPerTimeSlot}{" "}
{t("seats_available", {
@@ -1,162 +0,0 @@
import type { FC, ReactNode } from "react";
import { useEffect } from "react";
import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { Badge } from "@calcom/ui";
import { CheckSquare, Clock } from "@calcom/ui/components/icon";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { UserAvatars } from "@components/booking/UserAvatars";
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
import type { AvailabilityPageProps } from "../../pages/[user]/[type]";
import type { BookPageProps } from "../../pages/[user]/book";
import type { DynamicAvailabilityPageProps } from "../../pages/d/[link]/[slug]";
import type { HashLinkPageProps } from "../../pages/d/[link]/book";
import type { AvailabilityTeamPageProps } from "../../pages/team/[slug]/[type]";
import type { TeamBookingPageProps } from "../../pages/team/[slug]/book";
import { AvailableEventLocations } from "./AvailableEventLocations";
interface Props {
profile:
| AvailabilityPageProps["profile"]
| HashLinkPageProps["profile"]
| TeamBookingPageProps["profile"]
| BookPageProps["profile"]
| AvailabilityTeamPageProps["profile"]
| DynamicAvailabilityPageProps["profile"];
eventType:
| AvailabilityPageProps["eventType"]
| HashLinkPageProps["eventType"]
| TeamBookingPageProps["eventType"]
| BookPageProps["eventType"]
| AvailabilityTeamPageProps["eventType"]
| DynamicAvailabilityPageProps["eventType"];
isBookingPage?: boolean;
children: ReactNode;
isMobile?: boolean;
rescheduleUid?: string;
}
const BookingDescription: FC<Props> = (props) => {
const { profile, eventType, isBookingPage = false, children } = props;
const { date: bookingDate } = useRouterQuery("date");
const { t } = useLocale();
const { duration, setQuery: setDuration } = useRouterQuery("duration");
useEffect(() => {
if (
!duration ||
isNaN(Number(duration)) ||
(eventType.metadata?.multipleDuration &&
!eventType.metadata?.multipleDuration.includes(Number(duration)))
) {
setDuration(eventType.length);
}
}, [duration, setDuration, eventType.length, eventType.metadata?.multipleDuration]);
let requiresConfirmation = eventType?.requiresConfirmation;
let requiresConfirmationText = t("requires_confirmation");
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
if (rcThreshold) {
if (isBookingPage) {
if (dayjs(bookingDate).diff(dayjs(), rcThreshold.unit) > rcThreshold.time) {
requiresConfirmation = false;
}
} else {
requiresConfirmationText = t("requires_confirmation_threshold", {
...rcThreshold,
unit: rcThreshold.unit.slice(0, -1),
});
}
}
return (
<>
<UserAvatars
profile={profile}
users={eventType.users}
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
size="sm"
truncateAfter={3}
/>
<h2 className="text-default mb-2 mt-1 break-words text-sm font-medium ">
{eventType.team?.parent?.name} {profile.name}
</h2>
<h1 className="font-cal text-emphasis mb-6 break-words text-2xl font-semibold leading-none">
{eventType.title}
</h1>
<div className=" text-default flex flex-col space-y-4 text-sm font-medium">
{eventType?.description && (
<div
className={classNames(
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ",
isBookingPage && "text-default text-sm font-medium"
)}>
{/* TODO: Fix colors when token is introdcued to DS */}
<div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
<EventTypeDescriptionSafeHTML eventType={eventType} />
</div>
</div>
)}
{requiresConfirmation && (
<div className={classNames("items-top flex", isBookingPage && "text-default text-sm font-medium")}>
<div>
<CheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
</div>
{requiresConfirmationText}
</div>
)}
<AvailableEventLocations
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
/>
<div
className={classNames(
"flex flex-nowrap text-sm font-medium",
isBookingPage && "text-default",
!eventType.metadata?.multipleDuration && "items-center"
)}>
<Clock
className={classNames(
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
isBookingPage && "mt-[2px]"
)}
/>
{eventType.metadata?.multipleDuration !== undefined ? (
!isBookingPage ? (
<ul className="-mt-1 flex flex-wrap gap-1">
{eventType.metadata.multipleDuration.map((dur, idx) => (
<li key={idx}>
<Badge
variant="gray"
className={classNames(
duration === dur.toString()
? "bg-emphasis border-emphasis text-emphasis "
: "bg-subtle text-default border-transparent ",
"cursor-pointer border"
)}
onClick={() => {
setDuration(dur);
}}>
{dur} {t("minute_timeUnit")}
</Badge>
</li>
))}
</ul>
) : (
`${duration} ${t("minutes")}`
)
) : (
`${eventType.length} ${t("minutes")}`
)}
</div>
{children}
</div>
</>
);
};
export default BookingDescription;
@@ -10,6 +10,7 @@ import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog
import classNames from "@calcom/lib/classNames";
import { formatTime } from "@calcom/lib/date-fns";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { BookingStatus } from "@calcom/prisma/enums";
@@ -49,6 +50,8 @@ type BookingItemProps = BookingItem & {
function BookingListItem(booking: BookingItemProps) {
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const bookerUrl = useBookerUrl();
const user = query.data;
const {
t,
@@ -170,7 +173,7 @@ function BookingListItem(booking: BookingItemProps) {
id: "reschedule",
icon: Clock,
label: t("reschedule_booking"),
href: `/reschedule/${booking.uid}${
href: `${bookerUrl}/reschedule/${booking.uid}${
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
}`,
},
@@ -1,298 +0,0 @@
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { z } from "zod";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import dayjs from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
useEmbedStyles,
useEmbedUiConfig,
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import classNames from "@calcom/lib/classNames";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import notEmpty from "@calcom/lib/notEmpty";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc";
import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
import BookingDescription from "@components/booking/BookingDescription";
import { SlotPicker } from "@components/booking/SlotPicker";
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
/*const SlotPicker = dynamic(() => import("../SlotPicker").then((mod) => mod.SlotPicker), {
ssr: false,
loading: () => <div className="mt-8 px-4 pb-4 sm:mt-0 sm:p-4 md:min-w-[300px] md:px-5 lg:min-w-[455px]" />,
});*/
const TimezoneDropdown = dynamic(() => import("../TimezoneDropdown").then((mod) => mod.TimezoneDropdown), {
ssr: false,
});
const dateQuerySchema = z.object({
rescheduleUid: z.string().optional().default(""),
date: z.string().optional().default(""),
timeZone: z.string().optional().default(""),
seatReferenceUid: z.string().optional(),
});
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor: string; darkBrandColor: string }) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
};
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
const router = useRouter();
const isEmbed = useIsEmbed(restProps.isEmbed);
const query = dateQuerySchema.parse(router.query);
const { rescheduleUid } = query;
useTheme(profile.theme);
useBrandColors({
brandColor: profile.brandColor,
darkBrandColor: profile.darkBrandColor,
});
const { t, i18n } = useLocale();
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const isBackgroundTransparent = useIsBackgroundTransparent();
const [timeZone, setTimeZone] = useState<string>();
const [timeFormat, setTimeFormat] = useState<TimeFormat>(detectBrowserTimeFormat);
const onTimeFormatChange = (is24Hours: boolean) => {
setTimeFormat(is24Hours ? TimeFormat.TWENTY_FOUR_HOUR : TimeFormat.TWELVE_HOUR);
setIs24hClockInLocalStorage(is24Hours);
};
useEffect(() => {
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
}, []);
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
/*
const telemetry = useTelemetry();
useEffect(() => {
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
telemetryEventTypes.embedView,
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
);
}
}, [telemetry]); */
const embedUiConfig = useEmbedUiConfig();
// get dynamic user list here
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
const timezoneDropdown = useMemo(
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
[timeZone]
);
const paymentAppData = getPaymentAppData(eventType);
const rawSlug = profile.slug ? profile.slug.split("/") : [];
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
const { data: bookingAttendees } = trpc.viewer.bookings.getBookingAttendees.useQuery(
{
seatReferenceUid: rescheduleUid,
},
{
enabled: !!(rescheduleUid && eventType.seatsPerTimeSlot),
}
);
return (
<>
<HeadSeo
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
meeting={{
title: eventType.title,
profile: { name: `${profile.name}`, image: profile.image },
users: [
...(eventType.users || []).map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
],
}}
nextSeoProps={{
nofollow: eventType.hidden,
noindex: eventType.hidden,
}}
isBrandingHidden={restProps.isBrandingHidden}
/>
<BookingPageTagManager eventType={eventType} />
<div>
<main
className={classNames(
"flex flex-col md:mx-4",
shouldAlignCentrally ? "items-center" : "items-start",
!isEmbed && classNames("bg-subtle dark:bg-default mx-auto my-0 ease-in-out md:my-24")
)}>
<div>
<div
style={availabilityDatePickerEmbedStyles}
className={classNames(
isBackgroundTransparent ? "" : "bg-default dark:bg-muted pb-4 md:pb-0",
"border-booker md:border-booker-width md:rounded-md",
isEmbed && "mx-auto"
)}>
<div className="md:flex">
{showEventTypeDetails && (
<div
className={classNames(
" border-subtle flex flex-col p-5 sm:border-r",
"min-w-full md:w-[230px] md:min-w-[230px]",
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
)}>
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
{rescheduleUid && eventType.seatsPerTimeSlot && bookingAttendees && (
<div
className={classNames(
"flex flex-nowrap items-center text-sm font-medium",
" text-default",
"ltr:mr-[10px] rtl:ml-[10px]"
)}>
<User
className={classNames(
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
"mt-[2px]"
)}
/>{" "}
{t("event_type_seats", { numberOfSeats: bookingAttendees })}
</div>
)}
{!rescheduleUid && eventType.recurringEvent && (
<div className="flex items-start text-sm font-medium">
<RefreshCcw className="float-left ml-[2px] mt-[7px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<div>
<p className="-ml-2 mb-1 inline px-2 py-1">
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
</p>
<NumberInput
defaultValue={eventType.recurringEvent.count}
min="1"
max={eventType.recurringEvent.count}
isFullWidth={false}
className="me-2 inline w-16"
onChange={(event) => {
const count =
eventType?.recurringEvent?.count &&
parseInt(event?.target.value) > eventType?.recurringEvent?.count
? eventType.recurringEvent.count
: parseInt(event?.target.value);
setRecurringEventCount(count);
}}
/>
<p className="inline">
{t("occurrence", {
count: recurringEventCount,
})}
</p>
</div>
</div>
)}
{paymentAppData.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium">
<CreditCard className="-mt-1 ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{paymentAppData.paymentOption === "HOLD" ? (
<>
{t("no_show_fee_amount", {
amount: paymentAppData.price / 100.0,
formatParams: { amount: { currency: paymentAppData.currency } },
})}
</>
) : (
<>
{new Intl.NumberFormat(i18n.language, {
style: "currency",
currency: paymentAppData.currency,
}).format(paymentAppData.price / 100)}
</>
)}
</p>
)}
{timezoneDropdown}
</BookingDescription>
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
<div>
<p
className="mt-4 mb-3 text-default"
data-testid="former_time_p_desktop">
{t("former_time")}
</p>
<p className="text-subtle line-through ">
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-subtle" />
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
</p>
</div>
)*/}
</div>
)}
<SlotPicker
weekStart={
typeof profile.weekStart === "string"
? ([
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
].indexOf(profile.weekStart) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
: profile.weekStart /* Allows providing weekStart as number */
}
eventType={eventType}
timeFormat={timeFormat}
onTimeFormatChange={onTimeFormatChange}
timeZone={timeZone}
users={userList}
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
bookingAttendees={bookingAttendees || undefined}
recurringEventCount={recurringEventCount}
/>
</div>
</div>
{/* FIXME: We don't show branding in Embed yet because we need to place branding on top of the main content. Keeping it outside the main content would have visibility issues because outside main content background is transparent */}
{!restProps.isBrandingHidden && !isEmbed && <PoweredBy />}
</div>
</main>
</div>
<Toaster position="bottom-right" />
</>
);
};
export default AvailabilityPage;
@@ -1,704 +0,0 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { useSession } from "next-auth/react";
import dynamic from "next/dynamic";
import Head from "next/head";
import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { useForm, useFormContext } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import type { EventLocationType } from "@calcom/app-store/locations";
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import type { LocationObject } from "@calcom/core/location";
import dayjs from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
useEmbedUiConfig,
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { createBooking, createRecurringBooking } from "@calcom/features/bookings/lib";
import { useTimePreferences } from "@calcom/features/bookings/lib";
import {
getBookingFieldsWithSystemFields,
SystemField,
} from "@calcom/features/bookings/lib/getBookingFields";
import getBookingResponsesSchema, {
getBookingResponsesPartialSchema,
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import classNames from "@calcom/lib/classNames";
import { APP_NAME, MINUTES_TO_BOOK } from "@calcom/lib/constants";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { parseDate, parseDateTimeWithTimeZone, parseRecurringDates } from "@calcom/lib/parse-dates";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc";
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import BookingDescription from "@components/booking/BookingDescription";
import type { BookPageProps } from "../../../pages/[user]/book";
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
/** These are like 40kb that not every user needs */
const BookingDescriptionPayment = dynamic(
() => import("@components/booking/BookingDescriptionPayment")
) as unknown as typeof import("@components/booking/BookingDescriptionPayment").default;
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor?: string; darkBrandColor?: string }) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
};
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
const BookingFields = ({
fields,
locations,
rescheduleUid,
isDynamicGroupBooking,
}: {
fields: BookingPageProps["eventType"]["bookingFields"];
locations: LocationObject[];
rescheduleUid?: string;
isDynamicGroupBooking: boolean;
}) => {
const { t } = useLocale();
const { watch, setValue } = useFormContext();
const locationResponse = watch("responses.location");
const currentView = rescheduleUid ? "reschedule" : "";
return (
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
<div>
{fields.map((field, index) => {
// During reschedule by default all system fields are readOnly. Make them editable on case by case basis.
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
let readOnly =
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
let noLabel = false;
let hidden = !!field.hidden;
const fieldViews = field.views;
if (fieldViews && !fieldViews.find((view) => view.id === currentView)) {
return null;
}
if (field.name === SystemField.Enum.rescheduleReason) {
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
readOnly = false;
}
if (field.name === SystemField.Enum.smsReminderNumber) {
// `smsReminderNumber` and location.optionValue when location.value===phone are the same data point. We should solve it in a better way in the Form Builder itself.
// I think we should have a way to connect 2 fields together and have them share the same value in Form Builder
if (locationResponse?.value === "phone") {
setValue(`responses.${SystemField.Enum.smsReminderNumber}`, locationResponse?.optionValue);
// Just don't render the field now, as the value is already connected to attendee phone location
return null;
}
// `smsReminderNumber` can be edited during reschedule even though it's a system field
readOnly = false;
}
if (field.name === SystemField.Enum.guests) {
// No matter what user configured for Guests field, we don't show it for dynamic group booking as that doesn't support guests
hidden = isDynamicGroupBooking ? true : !!field.hidden;
}
// We don't show `notes` field during reschedule
if (
(field.name === SystemField.Enum.notes || field.name === SystemField.Enum.guests) &&
!!rescheduleUid
) {
return null;
}
// Dynamically populate location field options
if (field.name === SystemField.Enum.location && field.type === "radioInput") {
if (!field.optionsInputs) {
throw new Error("radioInput must have optionsInputs");
}
const optionsInputs = field.optionsInputs;
// TODO: Instead of `getLocationOptionsForSelect` options should be retrieved from dataStore[field.getOptionsAt]. It would make it agnostic of the `name` of the field.
const options = getLocationOptionsForSelect(locations, t);
options.forEach((option) => {
const optionInput = optionsInputs[option.value as keyof typeof optionsInputs];
if (optionInput) {
optionInput.placeholder = option.inputPlaceholder;
}
});
field.options = options.filter(
(location): location is NonNullable<(typeof options)[number]> => !!location
);
// If we have only one option and it has an input, we don't show the field label because Option name acts as label.
// e.g. If it's just Attendee Phone Number option then we don't show `Location` label
if (field.options.length === 1) {
if (field.optionsInputs[field.options[0].value]) {
noLabel = true;
} else {
// If there's only one option and it doesn't have an input, we don't show the field at all because it's visible in the left side bar
hidden = true;
}
}
}
const label = noLabel ? "" : field.label || t(field.defaultLabel || "");
const placeholder = field.placeholder || t(field.defaultPlaceholder || "");
return (
<FormBuilderField
className="mb-4"
field={{ ...field, label, placeholder, hidden }}
readOnly={readOnly}
key={index}
/>
);
})}
</div>
);
};
const routerQuerySchema = z
.object({
timeFormat: z.nativeEnum(TimeFormat),
rescheduleUid: z.string().optional(),
date: z
.string()
.optional()
.transform((date) => {
if (date === undefined) {
return null;
}
return date;
}),
})
.passthrough();
const BookingPage = ({
eventType,
booking,
currentSlotBooking,
profile,
isDynamicGroupBooking,
recurringEventCount,
hasHashedBookingLink,
hashedLink,
...restProps
}: BookingPageProps) => {
const removeSelectedSlotMarkMutation = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation();
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const { t, i18n } = useLocale();
const { duration: queryDuration } = useRouterQuery("duration");
const { date: queryDate } = useRouterQuery("date");
const isEmbed = useIsEmbed(restProps.isEmbed);
const embedUiConfig = useEmbedUiConfig();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const router = useRouter();
const { data: session } = useSession();
const isBackgroundTransparent = useIsBackgroundTransparent();
const telemetry = useTelemetry();
const { timezone } = useTimePreferences();
const reserveSlot = () => {
if (queryDuration) {
reserveSlotMutation.mutate({
eventTypeId: eventType.id,
slotUtcStartDate: dayjs(queryDate).utc().format(),
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
bookingUid: currentSlotBooking?.uid,
});
}
};
// Define duration now that we support multiple duration eventTypes
let duration = eventType.length;
if (
queryDuration &&
!isNaN(Number(queryDuration)) &&
eventType.metadata?.multipleDuration &&
eventType.metadata?.multipleDuration.includes(Number(queryDuration))
) {
duration = Number(queryDuration);
}
useEffect(() => {
/* if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
telemetryEventTypes.embedView,
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
);
} */
reserveSlot();
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
return () => {
clearInterval(interval);
removeSelectedSlotMarkMutation.mutate();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const mutation = useMutation(createBooking, {
onSuccess: async (responseData) => {
const { uid } = responseData;
if ("paymentUid" in responseData && !!responseData.paymentUid) {
return await router.push(
createPaymentLink({
paymentUid: responseData.paymentUid,
date,
name: bookingForm.getValues("responses.name"),
email: bookingForm.getValues("responses.email"),
absolute: false,
})
);
}
const query = {
isSuccessBookingPage: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
};
return bookingSuccessRedirect({
router,
successRedirectUrl: eventType.successRedirectUrl,
query,
bookingUid: uid,
});
},
});
const recurringMutation = useMutation(createRecurringBooking, {
onSuccess: async (responseData = []) => {
const { uid } = responseData[0] || {};
const query = {
isSuccessBookingPage: true,
allRemainingBookings: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
formerTime: booking?.startTime.toString(),
};
return bookingSuccessRedirect({
router,
successRedirectUrl: eventType.successRedirectUrl,
query,
bookingUid: uid,
});
},
});
const {
data: { timeFormat, rescheduleUid, date },
} = useTypedQuery(routerQuerySchema);
useTheme(profile.theme);
useBrandColors({
brandColor: profile.brandColor,
darkBrandColor: profile.darkBrandColor,
});
const querySchema = getBookingResponsesPartialSchema({
eventType: {
bookingFields: getBookingFieldsWithSystemFields(eventType),
},
view: rescheduleUid ? "reschedule" : "booking",
});
const parsedQuery = querySchema.parse({
...router.query,
// `guest` because we need to support legacy URL with `guest` query param support
// `guests` because the `name` of the corresponding bookingField is `guests`
guests: router.query.guests || router.query.guest,
});
// it would be nice if Prisma at some point in the future allowed for Json<Location>; as of now this is not the case.
const locations: LocationObject[] = useMemo(
() => (eventType.locations as LocationObject[]) || [],
[eventType.locations]
);
const [isClientTimezoneAvailable, setIsClientTimezoneAvailable] = useState(false);
useEffect(() => {
// THis is to fix hydration error that comes because of different timezone on server and client
setIsClientTimezoneAvailable(true);
}, []);
const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id;
// There should only exists one default userData variable for primaryAttendee.
const defaultUserValues = {
email: rescheduleUid ? booking?.attendees[0].email : parsedQuery["email"],
name: rescheduleUid ? booking?.attendees[0].name : parsedQuery["name"],
};
const defaultValues = () => {
if (!rescheduleUid) {
const defaults = {
responses: {} as Partial<z.infer<typeof bookingFormSchema>["responses"]>,
};
const responses = eventType.bookingFields.reduce((responses, field) => {
return {
...responses,
[field.name]: parsedQuery[field.name],
};
}, {});
defaults.responses = {
...responses,
name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "",
email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "",
};
return defaults;
}
if (!booking || !booking.attendees.length) {
return {};
}
const primaryAttendee = booking.attendees[0];
if (!primaryAttendee) {
return {};
}
const defaults = {
responses: {} as Partial<z.infer<typeof bookingFormSchema>["responses"]>,
};
const responses = eventType.bookingFields.reduce((responses, field) => {
return {
...responses,
[field.name]: booking.responses[field.name],
};
}, {});
defaults.responses = {
...responses,
name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "",
email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "",
};
return defaults;
};
const bookingFormSchema = z
.object({
responses: getBookingResponsesSchema({
eventType: { bookingFields: getBookingFieldsWithSystemFields(eventType) },
view: rescheduleUid ? "reschedule" : "booking",
}),
})
.passthrough();
type BookingFormValues = {
locationType?: EventLocationType["type"];
responses: z.infer<typeof bookingFormSchema>["responses"];
};
const bookingForm = useForm<BookingFormValues>({
defaultValues: defaultValues(),
resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema
});
// Calculate the booking date(s)
let recurringStrings: string[] = [],
recurringDates: Date[] = [];
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
[recurringStrings, recurringDates] = parseRecurringDates(
{
startDate: date,
timeZone: timeZone(),
recurringEvent: eventType.recurringEvent,
recurringCount: parseInt(recurringEventCount.toString()),
selectedTimeFormat: timeFormat,
},
i18n.language
);
}
const bookEvent = (bookingValues: BookingFormValues) => {
telemetry.event(
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
{ isTeamBooking: document.URL.includes("team/") }
);
// "metadata" is a reserved key to allow for connecting external users without relying on the email address.
// <...url>&metadata[user_id]=123 will be send as a custom input field as the hidden type.
// @TODO: move to metadata
const metadata = Object.keys(router.query)
.filter((key) => key.startsWith("metadata"))
.reduce(
(metadata, key) => ({
...metadata,
[key.substring("metadata[".length, key.length - 1)]: router.query[key],
}),
{}
);
if (recurringDates.length) {
// Identify set of bookings to one intance of recurring event to support batch changes
const recurringEventId = uuidv4();
const recurringBookings = recurringDates.map((recurringDate) => ({
...bookingValues,
start: dayjs(recurringDate).utc().format(),
end: dayjs(recurringDate).utc().add(duration, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
recurringEventId,
// Added to track down the number of actual occurrences selected by the user
recurringCount: recurringDates.length,
timeZone: timeZone(),
language: i18n.language,
rescheduleUid,
user: router.query.user,
metadata,
hasHashedBookingLink,
hashedLink,
}));
recurringMutation.mutate(recurringBookings);
} else {
mutation.mutate({
...bookingValues,
start: dayjs(date).utc().format(),
end: dayjs(date).utc().add(duration, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
timeZone: timeZone(),
language: i18n.language,
rescheduleUid,
bookingUid: (router.query.bookingUid as string) || booking?.uid,
user: router.query.user,
metadata,
hasHashedBookingLink,
hashedLink,
seatReferenceUid: router.query.seatReferenceUid as string,
});
}
};
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
return (
<>
<Head>
<title>
{rescheduleUid
? t("booking_reschedule_confirmation", {
eventTypeTitle: eventType.title,
profileName: profile.name,
})
: t("booking_confirmation", {
eventTypeTitle: eventType.title,
profileName: profile.name,
})}{" "}
| {APP_NAME}
</title>
<link rel="icon" href="/favico.ico" />
</Head>
<BookingPageTagManager eventType={eventType} />
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "" : "sm:my-24",
"my-0 max-w-3xl"
)}>
<div
className={classNames(
"main",
isBackgroundTransparent ? "" : "bg-default dark:bg-muted",
"border-booker sm:border-booker-width rounded-md"
)}>
<div className="sm:flex">
{showEventTypeDetails && (
<div className="sm:border-subtle text-default flex flex-col px-6 pb-0 pt-6 sm:w-1/2 sm:border-r sm:pb-6">
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
<BookingDescriptionPayment eventType={eventType} t={t} i18n={i18n} />
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
<div className="text-default items-start text-sm font-medium">
<RefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<p className="-ml-2 inline-block items-center px-2">
{getEveryFreqFor({
t,
recurringEvent: eventType.recurringEvent,
recurringCount: recurringEventCount,
})}
</p>
</div>
)}
<div className="text-bookinghighlight flex items-start text-sm">
<Calendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<div className="text-sm font-medium">
{isClientTimezoneAvailable &&
(rescheduleUid || !eventType.recurringEvent?.freq) &&
`${parseDate(date, i18n.language, { selectedTimeFormat: timeFormat })}`}
{isClientTimezoneAvailable &&
!rescheduleUid &&
eventType.recurringEvent?.freq &&
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
return <p key={key}>{timeFormatted}</p>;
})}
{!rescheduleUid && eventType?.recurringEvent?.freq && recurringStrings.length > 5 && (
<div className="flex">
<Tooltip
content={recurringStrings.slice(5).map((timeFormatted, key) => (
<p key={key}>{timeFormatted}</p>
))}>
<p className=" text-sm">
+ {t("plus_more", { count: recurringStrings.length - 5 })}
</p>
</Tooltip>
</div>
)}
</div>
</div>
{booking?.startTime && rescheduleUid && (
<div>
<p className="mb-2 mt-8 text-sm " data-testid="former_time_p">
{t("former_time")}
</p>
<p className="line-through ">
<Calendar className="-mt-1 ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{isClientTimezoneAvailable &&
typeof booking.startTime === "string" &&
parseDateTimeWithTimeZone(booking.startTime, i18n.language, timezone, {
selectedTimeFormat: timeFormat,
})}
</p>
</div>
)}
{!!eventType.seatsPerTimeSlot && (
<div className="text-bookinghighlight flex items-start text-sm">
<User
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600"
: currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-bookinghighlight"
}`}
/>
<p
className={`${
currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600"
: currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-bookinghighlight"
} mb-2 font-medium`}>
{currentSlotBooking
? eventType.seatsPerTimeSlot - currentSlotBooking.attendees.length
: eventType.seatsPerTimeSlot}{" "}
/ {eventType.seatsPerTimeSlot}{" "}
{t("seats_available", {
count: currentSlotBooking
? eventType.seatsPerTimeSlot - currentSlotBooking.attendees.length
: eventType.seatsPerTimeSlot,
})}
</p>
</div>
)}
</BookingDescription>
</div>
)}
<div className={classNames("p-6", showEventTypeDetails ? "sm:w-1/2" : "w-full")}>
<Form form={bookingForm} noValidate handleSubmit={bookEvent}>
<BookingFields
isDynamicGroupBooking={isDynamicGroupBooking}
fields={eventType.bookingFields}
locations={locations}
rescheduleUid={rescheduleUid}
/>
<div
className={classNames(
"flex justify-end space-x-2 rtl:space-x-reverse",
// HACK: If the last field is guests, we need to move Cancel, Submit buttons up because "Add Guests" being present on the left and the buttons on the right, spacing is not required
eventType.bookingFields[eventType.bookingFields.length - 1].name ===
SystemField.Enum.guests
? "-mt-4"
: ""
)}>
<Button color="minimal" type="button" onClick={() => router.back()}>
{t("cancel")}
</Button>
<Button
type="submit"
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
loading={mutation.isLoading || recurringMutation.isLoading}>
{rescheduleUid ? t("reschedule") : t("confirm")}
</Button>
</div>
</Form>
{mutation.isError || recurringMutation.isError ? (
<ErrorMessage error={mutation.error || recurringMutation.error} />
) : null}
</div>
</div>
</div>
</main>
<Toaster position="bottom-right" />
</>
);
};
export default BookingPage;
function ErrorMessage({ error }: { error: unknown }) {
const { t } = useLocale();
const { query: { rescheduleUid } = {} } = useRouter();
const router = useRouter();
return (
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<AlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
</div>
<div className="ms-3">
<p className="text-sm text-blue-700">
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
{error instanceof HttpError || error instanceof Error ? (
<>
{t("can_you_try_again")}{" "}
<span className="cursor-pointer underline" onClick={() => router.back()}>
{t("go_back")}
</span>
.
</> /* t(error.message) */
) : (
"Unknown error"
)}
</p>
</div>
</div>
</div>
);
}
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import type { z } from "zod";
import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } from "@calcom/core/event";
@@ -16,13 +17,23 @@ import {
allowDisablingHostConfirmationEmails,
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import type { EditableSchema } from "@calcom/features/form-builder/schema";
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
import {
Button,
CheckboxField,
Label,
SettingsToggle,
showToast,
TextField,
Tooltip,
Alert,
} from "@calcom/ui";
import { Edit, Copy } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -85,6 +96,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
return {
...field,
hidden: !enabled,
editable: (!enabled ? "system-but-hidden" : "system-but-optional") as z.infer<
typeof EditableSchema
>,
};
}
return field;
@@ -337,7 +351,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
}}
/>
<div className="mt-2">
<Checkbox
<CheckboxField
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
+16 -2
View File
@@ -1,3 +1,5 @@
import { Trans } from "next-i18next";
import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useFormContext } from "react-hook-form";
@@ -158,9 +160,21 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
{!shouldLockDisableProps("apps").disabled && (
<div className="bg-muted rounded-md p-8">
{!isLoading && notInstalledApps?.length ? (
<h2 className="text-emphasis text-lg font-semibold">{t("available_apps")}</h2>
<>
<h2 className="text-emphasis mb-2 text-xl font-semibold leading-5 tracking-[0.01em]">
{t("available_apps_lower_case")}
</h2>
<p className="text-default mb-6 text-sm font-normal">
<Trans i18nKey="available_apps_desc">
You have no apps installed. View popular apps below and explore more in our &nbsp;
<Link className="cursor-pointer underline" href="/apps">
App Store
</Link>
</Trans>
</p>
</>
) : null}
<div className="before:border-0">
<div className="bg-default border-subtle divide-subtle divide-y rounded-md border before:border-0">
{notInstalledApps?.map((app) => (
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
@@ -1,5 +1,5 @@
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { useState, memo } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type { OptionProps, SingleValueProps } from "react-select";
import { components } from "react-select";
@@ -71,86 +71,91 @@ const format = (date: Date, hour12: boolean) =>
hourCycle: hour12 ? "h12" : "h24",
}).format(new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")));
const EventTypeScheduleDetails = ({
isManagedEventType,
selectedScheduleValue,
}: {
isManagedEventType: boolean;
selectedScheduleValue: AvailabilityOption | undefined;
}) => {
const { data: loggedInUser } = useMeQuery();
const timeFormat = loggedInUser?.timeFormat;
const { t, i18n } = useLocale();
const { watch } = useFormContext<FormValues>();
const EventTypeScheduleDetails = memo(
({
isManagedEventType,
selectedScheduleValue,
}: {
isManagedEventType: boolean;
selectedScheduleValue: AvailabilityOption | undefined;
}) => {
const { data: loggedInUser } = useMeQuery();
const timeFormat = loggedInUser?.timeFormat;
const { t, i18n } = useLocale();
const { watch } = useFormContext<FormValues>();
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{
scheduleId: scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
isManagedEventType,
},
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
);
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{
scheduleId:
scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
isManagedEventType,
},
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
);
const filterDays = (dayNum: number) =>
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
const filterDays = (dayNum: number) =>
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
return (
<div className="border-default space-y-4 rounded border px-6 pb-4">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
return (
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span
className={classNames(
"w-20 font-medium sm:w-32 ",
!isAvailable ? "text-subtle line-through" : "text-default"
)}>
{day}
</span>
{isLoading ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="text-default flex items-center leading-4">
<span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ms-4">-</span>
<div className="ml-6 sm:w-28">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
</div>
) : (
<span className="text-subtle ml-6 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr className="border-subtle" />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
{!!schedule?.id && !schedule.isManaged && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
color="minimal"
EndIcon={ExternalLink}
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
)}
return (
<div className="border-default space-y-4 rounded border px-6 pb-4">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
return (
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span
className={classNames(
"w-20 font-medium sm:w-32 ",
!isAvailable ? "text-subtle line-through" : "text-default"
)}>
{day}
</span>
{isLoading ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="text-default flex items-center leading-4">
<span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ms-4">-</span>
<div className="ml-6 sm:w-28">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
</div>
) : (
<span className="text-subtle ml-6 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr className="border-subtle" />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
{!!schedule?.id && !schedule.isManaged && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
color="minimal"
EndIcon={ExternalLink}
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
)}
</div>
</div>
</div>
);
};
);
}
);
EventTypeScheduleDetails.displayName = "EventTypeScheduleDetails";
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
+12 -7
View File
@@ -19,16 +19,17 @@ interface IUserToValue {
email: string;
}
const mapUserToValue = ({ id, name, username, email }: IUserToValue) => ({
const mapUserToValue = ({ id, name, username, email }: IUserToValue, pendingString: string) => ({
value: `${id || ""}`,
label: `${name || ""}`,
label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`,
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
email,
});
export const mapMemberToChildrenOption = (
member: EventTypeSetupProps["teamMembers"][number],
slug: string
slug: string,
pendingString: string
) => {
return {
slug,
@@ -43,7 +44,7 @@ export const mapMemberToChildrenOption = (
eventTypeSlugs: member.eventTypes ?? [],
},
value: `${member.id ?? ""}`,
label: member.name ?? "",
label: `${member.name || member.email || ""}${!member.username ? ` (${pendingString})` : ""}`,
};
};
@@ -291,9 +292,13 @@ export const EventTeamTab = ({
// description: t("round_robin_description"),
},
];
const teamMembersOptions = teamMembers.map(mapUserToValue);
const childrenEventTypeOptions = teamMembers.map((member) => {
return mapMemberToChildrenOption(member, eventType.slug);
const pendingMembers = (member: (typeof teamMembers)[number]) =>
!!eventType.team?.parentId || !!member.username;
const teamMembersOptions = teamMembers
.filter(pendingMembers)
.map((member) => mapUserToValue(member, t("pending")));
const childrenEventTypeOptions = teamMembers.filter(pendingMembers).map((member) => {
return mapMemberToChildrenOption(member, eventType.slug, t("pending"));
});
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
return (
@@ -7,6 +7,7 @@ import { useMemo, useState, Suspense } from "react";
import type { UseFormReturn } from "react-hook-form";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
@@ -208,10 +209,8 @@ function EventTypeSingleLayout({
}`,
});
}
if (isManagedEventType || isChildrenManagedEventType) {
// Removing apps and workflows for manageg event types by admins v1
navigation.splice(0, 1);
} else {
const showWebhooks = !(isManagedEventType || isChildrenManagedEventType);
if (showWebhooks) {
navigation.push({
name: "webhooks",
href: `/event-types/${eventType.id}?tabName=webhooks`,
@@ -233,9 +232,11 @@ function EventTypeSingleLayout({
formMethods,
]);
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
eventType.slug
}`;
const orgBranding = useOrgBranding();
const isOrgEvent = orgBranding?.fullDomain;
const permalink = `${orgBranding?.fullDomain ?? CAL_URL}/${
team ? `${!isOrgEvent ? "team/" : ""}${team.slug}` : eventType.users[0].username
}/${eventType.slug}`;
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
@@ -315,6 +316,7 @@ function EventTypeSingleLayout({
color="secondary"
variant="icon"
tooltip={t("embed")}
eventId={eventType.id}
/>
</>
)}
@@ -404,13 +406,13 @@ function EventTypeSingleLayout({
className="primary-navigation"
tabs={EventTypeTabs}
sticky
linkProps={{ shallow: true }}
linkShallow
itemClassname="items-start"
iconClassName="md:mt-px"
/>
</div>
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
<HorizontalTabs tabs={EventTypeTabs} linkProps={{ shallow: true }} />
<HorizontalTabs tabs={EventTypeTabs} linkShallow />
</div>
<div className="w-full ltr:mr-2 rtl:ml-2">
<div
@@ -28,6 +28,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
color="secondary"
disabled={installed}
type="button"
loading={buttonProps?.isLoading}
onClick={(event) => {
// Save cookie key to return url step
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
@@ -17,7 +17,11 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
const { nextStep } = props;
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery({ onboarding: true });
const { t } = useLocale();
const queryIntegrations = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
const queryIntegrations = trpc.viewer.integrations.useQuery({
variant: "calendar",
onlyInstalled: false,
sortByMostPopular: true,
});
const firstCalendar = queryConnectedCalendars.data?.connectedCalendars.find(
(item) => item.calendars && item.calendars?.length > 0
@@ -16,6 +16,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
const { data: queryConnectedVideoApps, isLoading } = trpc.viewer.integrations.useQuery({
variant: "conferencing",
onlyInstalled: false,
sortByMostPopular: true,
});
const { t } = useLocale();
@@ -22,15 +22,13 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const { nextStep } = props;
const router = useRouter();
let queryAvailability;
if (defaultScheduleId) {
queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: defaultScheduleId },
{
enabled: router.isReady,
}
);
}
const queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: defaultScheduleId! },
{
enabled: router.isReady && !!defaultScheduleId,
}
);
const availabilityForm = useForm({
defaultValues: {
@@ -15,6 +15,7 @@ import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
interface IUserSettingsProps {
nextStep: () => void;
hideUsername?: boolean;
}
const UserSettings = (props: IUserSettingsProps) => {
@@ -66,8 +67,8 @@ const UserSettings = (props: IUserSettingsProps) => {
return (
<form onSubmit={onSubmit}>
<div className="space-y-6">
{/* Username textfield */}
<UsernameAvailabilityField />
{/* Username textfield: when not coming from signup */}
{!props.hideUsername && <UsernameAvailabilityField />}
{/* Full name textfield */}
<div className="w-full">
@@ -1,8 +1,9 @@
import type { BaseSyntheticEvent } from "react";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useCallbackRef } from "@calcom/lib/hooks/useCallbackRef";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form } from "@calcom/ui";
@@ -127,6 +128,17 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
}
}
const handleEnableRef = useCallbackRef(handleEnable);
const totpCode = form.watch("totpCode");
// auto submit 2FA if all inputs have a value
useEffect(() => {
if (totpCode?.trim().length === 6) {
form.handleSubmit(handleEnableRef.current)();
}
}, [form, handleEnableRef, totpCode]);
return (
<Dialog open={true}>
<DialogContent>
@@ -12,7 +12,7 @@ import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
interface DisableTwoFactorAuthModalProps {
open: boolean;
onOpenChange: () => void;
disablePassword?: boolean;
/** Called when the user closes the modal without disabling two-factor auth */
onCancel: () => void;
/** Called when the user disables two-factor auth */
@@ -27,6 +27,7 @@ interface DisableTwoFactorValues {
const DisableTwoFactorAuthModal = ({
onDisable,
onCancel,
disablePassword,
open,
onOpenChange,
}: DisableTwoFactorAuthModalProps) => {
@@ -75,19 +76,21 @@ const DisableTwoFactorAuthModal = ({
<DialogContent title={t("disable_2fa")} description={t("disable_2fa_recommendation")} type="creation">
<Form form={form} handleSubmit={handleDisable}>
<div className="mb-8">
<PasswordField
labelProps={{
className: "block text-sm font-medium text-default",
}}
{...form.register("password")}
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
/>
{!disablePassword && (
<PasswordField
labelProps={{
className: "block text-sm font-medium text-default",
}}
{...form.register("password")}
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
/>
)}
<TwoFactor center={false} />
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
</div>
<DialogFooter showDivider className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<DialogFooter showDivider className="relative mt-5">
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
@@ -1,8 +1,9 @@
import type { BaseSyntheticEvent } from "react";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useCallbackRef } from "@calcom/lib/hooks/useCallbackRef";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, DialogFooter, Form, TextField } from "@calcom/ui";
@@ -129,6 +130,17 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
}
}
const handleEnableRef = useCallbackRef(handleEnable);
const totpCode = form.watch("totpCode");
// auto submit 2FA if all inputs have a value
useEffect(() => {
if (totpCode?.trim().length === 6) {
form.handleSubmit(handleEnableRef.current)();
}
}, [form, handleEnableRef, totpCode]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent title={t("enable_2fa")} description={setupDescriptions[step]} type="creation">
@@ -173,7 +185,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
)}
</div>
</WithStep>
<DialogFooter className="mt-8 sm:flex sm:flex-row-reverse" showDivider>
<DialogFooter className="mt-8" showDivider>
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
+14 -19
View File
@@ -1,11 +1,10 @@
import * as RadioGroup from "@radix-ui/react-radio-group";
import classNames from "classnames";
import Link from "next/link";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
const ENTERPRISE_BOOKING_FEE = "$99"; // TODO: get this from a new API endpoint
const ChooseLicense = (
props: {
value: string;
@@ -50,23 +49,19 @@ const ChooseLicense = (
</ul>
</div>
</RadioGroup.Item>
<RadioGroup.Item value="EE">
<div
className={classNames(
"bg-default cursor-pointer space-y-2 rounded-md border p-4 hover:border-black",
value === "EE" && "ring-2 ring-black"
)}>
<h2 className="font-cal text-emphasis text-xl">{t("ee_enterprise_license")}</h2>
<p className="font-medium text-green-800">
{t("enterprise_booking_fee", { enterprise_booking_fee: ENTERPRISE_BOOKING_FEE })}
</p>
<p className="text-subtle">{t("enterprise_license_includes")}</p>
<ul className="text-subtle ml-4 list-disc text-left text-xs">
<li>{t("no_need_to_keep_your_code_open_source")}</li>
<li>{t("repackage_rebrand_resell")}</li>
<li>{t("a_vast_suite_of_enterprise_features")}</li>
</ul>
</div>
<RadioGroup.Item value="EE" disabled>
<Link href="https://cal.com/sales" target="_blank">
<div className={classNames("bg-default h-full cursor-pointer space-y-2 rounded-md border p-4")}>
<h2 className="font-cal text-emphasis text-xl">{t("custom_plan")}</h2>
<p className="font-medium text-green-800">{t("contact_sales")}</p>
<p className="text-subtle">Build on top of Cal.com</p>
<ul className="text-subtle ml-4 list-disc text-left text-xs">
<li>{t("no_need_to_keep_your_code_open_source")}</li>
<li>{t("repackage_rebrand_resell")}</li>
<li>{t("a_vast_suite_of_enterprise_features")}</li>
</ul>
</div>
</Link>
</RadioGroup.Item>
</RadioGroup.Root>
</form>
+4 -9
View File
@@ -1,7 +1,6 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
@@ -12,24 +11,20 @@ type TeamType = NonNullable<TeamWithMembers>;
type MembersType = TeamType["members"];
type MemberType = MembersType[number] & { safeBio: string | null };
type TeamTypeWithSafeHtml = Omit<TeamType, "members"> & { members: MemberType[] };
type TeamTypeWithSafeHtml = Omit<TeamType, "members" | "inviteToken"> & { members: MemberType[] };
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
const { t } = useLocale();
const router = useRouter();
const isBioEmpty = !member.bio || !member.bio.replace("<p><br></p>", "").length;
// slug is a route parameter, we don't want to forward it to the next route
const { slug: _slug, ...queryParamsToForward } = router.query;
// We don't want to forward orgSlug and user which are route params to the next route
const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = router.query;
return (
<Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}>
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
<Avatar
size="md"
alt={member.name || ""}
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
/>
<Avatar size="md" alt={member.name || ""} imageSrc={"/" + member.username + "/avatar.png"} />
<section className="mt-2 line-clamp-4 w-full space-y-1">
<p className="text-default font-medium">{member.name}</p>
<div className="text-subtle line-clamp-3 overflow-ellipsis text-sm font-normal">
@@ -1,5 +1,6 @@
import classNames from "classnames";
import { debounce, noop } from "lodash";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import type { RefCallback } from "react";
import { useEffect, useMemo, useState } from "react";
@@ -46,6 +47,7 @@ const obtainNewUsernameChangeCondition = ({
const PremiumTextfield = (props: ICustomUsernameProps) => {
const { t } = useLocale();
const { data: session, update } = useSession();
const {
currentUsername,
setCurrentUsername = noop,
@@ -94,6 +96,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
const updateUsername = trpc.viewer.updateProfile.useMutation({
onSuccess: async () => {
onSuccessMutation && (await onSuccessMutation());
await update();
setOpenDialogSaveUsername(false);
},
onError: (error) => {
@@ -1,5 +1,6 @@
import classNames from "classnames";
import { debounce, noop } from "lodash";
import { useSession } from "next-auth/react";
import type { RefCallback } from "react";
import { useEffect, useMemo, useState } from "react";
@@ -23,6 +24,7 @@ interface ICustomUsernameProps {
const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.ComponentProps<typeof TextField>>) => {
const { t } = useLocale();
const { data: session, update } = useSession();
const {
currentUsername,
setCurrentUsername = noop,
@@ -69,6 +71,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
onSuccessMutation && (await onSuccessMutation());
setOpenDialogSaveUsername(false);
setCurrentUsername(inputUsernameValue);
await update();
},
onError: (error) => {
onErrorMutation && onErrorMutation(error);
@@ -156,23 +159,23 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
<DialogContent type="confirmation" Icon={Edit2} title={t("confirm_username_change_dialog_title")}>
<div className="flex flex-row">
<div className="mb-4 w-full pt-1">
<div className="bg-subtle flex w-full flex-wrap rounded-sm py-3 text-sm">
<div className="flex-1 px-2">
<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">
{currentUsername}
</p>
</div>
<div className="ml-6 flex-1">
<div>
<p className="text-subtle" data-testid="new-username">
{t("new_username")}
</p>
<p className="text-emphasis">{inputUsernameValue}</p>
<p className="text-emphasis mt-1">{inputUsernameValue}</p>
</div>
</div>
</div>
</div>
<div className="mt-4 flex flex-row-reverse gap-x-2">
<div className="ml-4 mt-4 flex flex-row-reverse gap-x-2">
<Button
type="button"
loading={updateUsernameMutation.isLoading}
+2 -2
View File
@@ -3,7 +3,7 @@ import type { GroupBase, Props, InputProps, SingleValue, MultiValue } from "reac
import ReactSelect, { components } from "react-select";
import classNames from "@calcom/lib/classNames";
import useTheme from "@calcom/lib/hooks/useTheme";
import { useGetTheme } from "@calcom/lib/hooks/useTheme";
export type SelectProps<
Option,
@@ -30,7 +30,7 @@ function Select<
Group extends GroupBase<Option> = GroupBase<Option>
>({ className, ...props }: SelectProps<Option, IsMulti, Group>) {
const [mounted, setMounted] = useState<boolean>(false);
const { resolvedTheme, forcedTheme } = useTheme();
const { resolvedTheme, forcedTheme } = useGetTheme();
const hasDarkTheme = !forcedTheme && resolvedTheme === "dark";
const darkThemeColors = {
/** Dark Theme starts */
+44 -8
View File
@@ -1,5 +1,6 @@
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { SessionProvider } from "next-auth/react";
import { useSession } from "next-auth/react";
import { EventCollectionProvider } from "next-collect/client";
import type { SSRConfig } from "next-i18next";
import { appWithTranslation } from "next-i18next";
@@ -10,7 +11,6 @@ import { useRouter } from "next/router";
import type { ComponentProps, PropsWithChildren, ReactNode } from "react";
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { FeatureProvider } from "@calcom/features/flags/context/provider";
@@ -48,7 +48,18 @@ type AppPropsWithChildren = AppProps & {
children: ReactNode;
};
const CustomI18nextProvider = (props: AppPropsWithChildren) => {
const getEmbedNamespace = (query: ReturnType<typeof useRouter>["query"]) => {
// Mostly embed query param should be available on server. Use that there.
// Use the most reliable detection on client
return typeof window !== "undefined" ? window.getEmbedNamespace() : (query.embed as string) || null;
};
// We dont need to pass nonce to the i18n provider - this was causing x2-x3 re-renders on a hard refresh
type AppPropsWithoutNonce = Omit<AppPropsWithChildren, "pageProps"> & {
pageProps: Omit<AppPropsWithChildren["pageProps"], "nonce">;
};
const CustomI18nextProvider = (props: AppPropsWithoutNonce) => {
/**
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
* We intend to not cache i18n query
@@ -87,7 +98,7 @@ const CalcomThemeProvider = (props: CalcomThemeProps) => {
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
// One such example is our Embeds Demo and Testing page at http://localhost:3100
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
const embedNamespace = getEmbedNamespace(router.query);
const isEmbedMode = typeof embedNamespace === "string";
return (
@@ -158,10 +169,10 @@ function getThemeProviderProps({
? ThemeSupport.None
: ThemeSupport.App;
const isBookingPageThemSupportRequired = themeSupport === ThemeSupport.Booking;
const isBookingPageThemeSupportRequired = themeSupport === ThemeSupport.Booking;
const themeBasis = props.themeBasis;
if ((isBookingPageThemSupportRequired || isEmbedMode) && !themeBasis) {
if ((isBookingPageThemeSupportRequired || isEmbedMode) && !themeBasis) {
console.warn(
"`themeBasis` is required for booking page theme support. Not providing it will cause theme flicker."
);
@@ -184,7 +195,7 @@ function getThemeProviderProps({
`embed-theme-${embedNamespace}${appearanceIdSuffix}${embedExplicitlySetThemeSuffix}`
: themeSupport === ThemeSupport.App
? "app-theme"
: isBookingPageThemSupportRequired
: isBookingPageThemeSupportRequired
? `booking-theme${appearanceIdSuffix}`
: undefined;
@@ -207,20 +218,45 @@ function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
}
function useOrgBrandingValues() {
const session = useSession();
const res = trpc.viewer.organizations.getBrand.useQuery(undefined, {
// Only fetch if we have a session to avoid flooding logs with errors
enabled: session.status === "authenticated",
});
if (res.status === "loading") {
return undefined;
}
if (res.status === "error") return null;
return res.data;
}
function OrgBrandProvider({ children }: { children: React.ReactNode }) {
const orgBrand = useOrgBrandingValues();
return <OrgBrandingProvider value={orgBrand}>{children}</OrgBrandingProvider>;
return <OrgBrandingProvider value={{ orgBrand }}>{children}</OrgBrandingProvider>;
}
const AppProviders = (props: AppPropsWithChildren) => {
const session = trpc.viewer.public.session.useQuery().data;
// No need to have intercom on public pages - Good for Page Performance
const isPublicPage = usePublicPage();
const { pageProps, ...rest } = props;
const { _nonce, ...restPageProps } = pageProps;
const propsWithoutNonce = {
pageProps: {
...restPageProps,
},
...rest,
};
const RemainingProviders = (
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<SessionProvider session={session || undefined}>
<CustomI18nextProvider {...props}>
<CustomI18nextProvider {...propsWithoutNonce}>
<TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
<CalcomThemeProvider
+1 -5
View File
@@ -10,10 +10,6 @@ import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry
const middleware: NextMiddleware = async (req) => {
const url = req.nextUrl;
const requestHeaders = new Headers(req.headers);
/**
* We are using env variable to toggle new-booker because using flags would be an unnecessary delay for booking pages
* Also, we can't easily identify the booker page requests here(to just fetch the flags for those requests)
*/
if (isIpInBanlist(req) && url.pathname !== "/api/nope") {
// DDOS Prevention: Immediately end request with no response - Avoids a redirect as well initiated by NextAuth on invalid callback
@@ -82,7 +78,7 @@ const middleware: NextMiddleware = async (req) => {
export const config = {
matcher: [
"/:path*",
"/((?!_next|.*avatar.png$|favicon.ico$).*)",
"/api/collect-events/:path*",
"/api/auth/:path*",
"/apps/routing_forms/:path*",
+21 -78
View File
@@ -5,11 +5,6 @@ const englishTranslation = require("./public/static/locales/en/common.json");
const { withAxiom } = require("next-axiom");
const { i18n } = require("./next-i18next.config");
const {
userTypeRoutePath,
teamTypeRoutePath,
privateLinkRoutePath,
embedUserTypeRoutePath,
embedTeamTypeRoutePath,
orgHostPath,
orgUserRoutePath,
orgUserTypeRoutePath,
@@ -285,85 +280,12 @@ const nextConfig = {
},
],
// Keep cookie based booker enabled just in case we disable new-booker globally
...[
{
source: userTypeRoutePath,
destination: "/new-booker/:user/:type",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
{
source: teamTypeRoutePath,
destination: "/new-booker/team/:slug/:type",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
{
source: privateLinkRoutePath,
destination: "/new-booker/d/:link/:slug",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
],
// Keep cookie based booker enabled to test new-booker embed in production
...[
{
source: embedUserTypeRoutePath,
destination: "/new-booker/:user/:type/embed",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
{
source: embedTeamTypeRoutePath,
destination: "/new-booker/team/:slug/:type/embed",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
],
/* TODO: have these files being served from another deployment or CDN {
source: "/embed/embed.js",
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
}, */
/**
* Enables new booker using cookie. It works even if NEW_BOOKER_ENABLED_FOR_NON_EMBED, NEW_BOOKER_ENABLED_FOR_EMBED are disabled
*/
];
// Enable New Booker for all Embed Requests
if (process.env.NEW_BOOKER_ENABLED_FOR_EMBED === "1") {
console.log("Enabling New Booker for Embed");
afterFiles.push(
...[
{
source: embedUserTypeRoutePath,
destination: "/new-booker/:user/:type/embed",
},
{
source: embedTeamTypeRoutePath,
destination: "/new-booker/team/:slug/:type/embed",
},
]
);
}
// Enable New Booker for All but embed Requests
if (process.env.NEW_BOOKER_ENABLED_FOR_NON_EMBED === "1") {
console.log("Enabling New Booker for Non-Embed");
afterFiles.push(
...[
{
source: userTypeRoutePath,
destination: "/new-booker/:user/:type",
},
{
source: teamTypeRoutePath,
destination: "/new-booker/team/:slug/:type",
},
{
source: privateLinkRoutePath,
destination: "/new-booker/d/:link/:slug",
},
]
);
}
return {
beforeFiles,
afterFiles,
@@ -524,6 +446,27 @@ const nextConfig = {
destination: "/apps/installed/conferencing",
permanent: true,
},
// OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL
...(process.env.NODE_ENV === "development" &&
// Safer to enable the redirect only when the user is opting to test out organizations
process.env.ORGANIZATIONS_ENABLED &&
// Prevent infinite redirect by checking that we aren't already on localhost
process.env.NEXT_PUBLIC_WEBAPP_URL !== "http://localhost:3000"
? [
{
has: [
{
type: "header",
key: "host",
value: "localhost:3000",
},
],
source: "/api/integrations/:args*",
destination: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/integrations/:args*`,
permanent: false,
},
]
: []),
];
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.0.14",
"version": "3.1.5",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -47,7 +47,7 @@
"@next/bundle-analyzer": "^13.1.6",
"@radix-ui/react-avatar": "^1.0.0",
"@radix-ui/react-collapsible": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-id": "^1.0.0",
"@radix-ui/react-popover": "^1.0.2",
@@ -58,6 +58,7 @@
"@radix-ui/react-tooltip": "^1.0.0",
"@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0",
"@tanem/react-nprogress": "^5.0.44",
"@tanstack/react-query": "^4.3.9",
"@tremor/react": "^2.0.0",
"@types/turndown": "^5.0.1",
@@ -89,7 +90,7 @@
"micro": "^10.0.1",
"mime-types": "^2.1.35",
"next": "^13.4.6",
"next-auth": "^4.20.1",
"next-auth": "^4.22.1",
"next-axiom": "^0.17.0",
"next-collect": "^0.2.1",
"next-i18next": "^13.2.2",
+17 -26
View File
@@ -1,13 +1,14 @@
import type { GetStaticPropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { orgDomainConfig, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
import { DOCS_URL, JOIN_DISCORD, WEBSITE_URL } from "@calcom/lib/constants";
import { DOCS_URL, JOIN_DISCORD, WEBSITE_URL, IS_CALCOM } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui";
import { BookOpen, Check, ChevronRight, FileText, Shield } from "@calcom/ui/components/icon";
import { Discord } from "@calcom/ui/components/icon/Discord";
import PageWrapper from "@components/PageWrapper";
@@ -58,16 +59,21 @@ export default function Custom404() {
// Accessing a non-existent team
setUsername(splitPath[2]);
setCurrentPageType(pageType.TEAM);
setUrl(`${WEBSITE_URL}/signup?callbackUrl=settings/teams/new%3Fslug%3D${username.replace("/", "")}`);
setUrl(
`${WEBSITE_URL}/signup?callbackUrl=settings/teams/new%3Fslug%3D${splitPath[2].replace("/", "")}`
);
} else {
setUsername(routerUsername);
setUrl(`${WEBSITE_URL}/signup?username=${username.replace("/", "")}`);
setUrl(`${WEBSITE_URL}/signup?username=${routerUsername.replace("/", "")}`);
}
} else {
setUsername(currentOrgDomain);
setCurrentPageType(pageType.ORG);
setUrl(
`${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${username.replace("/", "")}`
`${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${currentOrgDomain.replace(
"/",
""
)}`
);
}
}, []);
@@ -75,7 +81,6 @@ export default function Custom404() {
const isSuccessPage = router.asPath.startsWith("/booking");
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
const isSignup = router.asPath.startsWith("/signup");
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
/**
* If we're on 404 and the route is insights it means it is disabled
* TODO: Abstract this for all disabled features
@@ -201,14 +206,7 @@ export default function Custom404() {
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<svg
className="text-default h-6 w-6"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 127.14 96.36">
<title>Discord Logo</title>
<path d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z" />
</svg>
<Discord className="text-default h-6 w-6" />
</span>
</div>
<div className="min-w-0 flex-1">
@@ -249,7 +247,7 @@ export default function Custom404() {
<span className="mt-2 inline-block text-lg ">
{t("check_spelling_mistakes_or_go_back")}
</span>
) : isCalcom ? (
) : IS_CALCOM ? (
<a target="_blank" href={url} className="mt-2 inline-block text-lg" rel="noreferrer">
{t(`404_the_${currentPageType.toLowerCase()}`)}{" "}
<strong className="text-blue-500">
@@ -267,7 +265,7 @@ export default function Custom404() {
)}
</div>
<div className="mt-12">
{((!isSubpage && isCalcom) ||
{((!isSubpage && IS_CALCOM) ||
currentPageType === pageType.ORG ||
currentPageType === pageType.TEAM) && (
<ul role="list" className="my-4">
@@ -317,7 +315,7 @@ export default function Custom404() {
.filter((_, idx) => currentPageType === pageType.ORG || idx !== 0)
.map((link, linkIdx) => (
<li key={linkIdx} className="px-4 py-2">
<Link
<a
href={link.href}
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
@@ -337,7 +335,7 @@ export default function Custom404() {
<div className="flex-shrink-0 self-center">
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</Link>
</a>
</li>
))}
<li className="px-4 py-2">
@@ -346,14 +344,7 @@ export default function Custom404() {
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<svg
className="text-default h-6 w-6"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 127.14 96.36">
<title>Discord Logo</title>
<path d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z" />
</svg>
<Discord className="text-default h-6 w-6" />
</span>
</div>
<div className="min-w-0 flex-1">
+33 -118
View File
@@ -13,13 +13,7 @@ import {
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import { WEBAPP_URL } from "@calcom/lib/constants";
import defaultEvents, {
getDynamicEventDescription,
getGroupName,
getUsernameList,
getUsernameSlugLink,
} from "@calcom/lib/defaultEvents";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
@@ -27,7 +21,7 @@ import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
import { Avatar, HeadSeo } from "@calcom/ui";
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -39,16 +33,7 @@ import { ssrInit } from "@server/lib/ssr";
export type UserPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export function UserPage(props: UserPageProps) {
const {
users,
profile,
eventTypes,
isDynamicGroup,
dynamicNames,
dynamicUsernames,
isSingleUser,
markdownStrippedBio,
} = props;
const { users, profile, eventTypes, isSingleUser, markdownStrippedBio } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
useTheme(user.theme);
const { t } = useLocale();
@@ -56,47 +41,6 @@ export function UserPage(props: UserPageProps) {
const isBioEmpty = !user.bio || !user.bio.replace("<p><br></p>", "").length;
const groupEventTypes = props.users.some((user) => !user.allowDynamicBooking) ? (
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border ">
<div className="text-muted p-8 text-center">
<h2 className="font-cal text-default mb-2 text-3xl">{" " + t("unavailable")}</h2>
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled") as string}</p>
</div>
</div>
</div>
) : (
<ul>
{eventTypes.map((type, index) => (
<li
key={index}
className=" border-subtle bg-default dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
<ArrowRight className="text-emphasis absolute right-3 top-3 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
<Link
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
className="flex justify-between px-6 py-4"
data-testid="event-type-link">
<div className="flex-shrink">
<p className=" text-emphasis text-sm font-semibold">{type.title}</p>
<EventTypeDescription className="text-sm" eventType={type} />
</div>
<div className="mt-1 self-center">
<AvatarGroup
truncateAfter={4}
className="flex flex-shrink-0"
size="sm"
items={props.users.map((user) => ({
alt: user.name || "",
image: user.avatar,
}))}
/>
</div>
</Link>
</li>
))}
</ul>
);
const isEmbed = useIsEmbed(props.isEmbed);
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
@@ -118,14 +62,12 @@ export function UserPage(props: UserPageProps) {
return (
<>
<HeadSeo
title={isDynamicGroup ? dynamicNames.join(", ") : nameOrUsername}
description={isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : markdownStrippedBio}
title={nameOrUsername}
description={markdownStrippedBio}
meeting={{
title: isDynamicGroup ? "" : markdownStrippedBio,
title: markdownStrippedBio,
profile: { name: `${profile.name}`, image: null },
users: isDynamicGroup
? dynamicUsernames.map((username, index) => ({ username, name: dynamicNames[index] }))
: [{ username: `${user.username}`, name: `${user.name}` }],
users: [{ username: `${user.username}`, name: `${user.name}` }],
}}
/>
@@ -166,8 +108,6 @@ export function UserPage(props: UserPageProps) {
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
</div>
</div>
) : isDynamicGroup ? ( //When we deal with dynamic group (users > 1)
groupEventTypes
) : (
eventTypes.map((type) => (
<div
@@ -289,9 +229,24 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
});
const isDynamicGroup = usersWithoutAvatar.length > 1;
if (isDynamicGroup) {
return {
redirect: {
permanent: false,
destination: `/${usernameList.join("+")}/dynamic`,
},
} as {
redirect: {
permanent: false;
destination: string;
};
};
}
const users = usersWithoutAvatar.map((user) => ({
...user,
avatar: `${WEBAPP_URL}/${user.username}/avatar.png`,
avatar: `/${user.username}/avatar.png`,
}));
if (!users.length || (!isValidOrgDomain && !users.some((user) => user.organizationId === null))) {
@@ -301,45 +256,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
notFound: true;
};
}
const isDynamicGroup = users.length > 1;
if (isDynamicGroup) {
// sort and be in the same order as usernameList so first user is the first user in the list
users.sort((a, b) => {
const aIndex = (a.username && usernameList.indexOf(a.username)) || 0;
const bIndex = (b.username && usernameList.indexOf(b.username)) || 0;
return aIndex - bIndex;
});
}
const dynamicNames = isDynamicGroup
? users.map((user) => {
return user.name || "";
})
: [];
const [user] = users; //to be used when dealing with single user, not dynamic group
const profile = isDynamicGroup
? {
name: getGroupName(dynamicNames),
image: null,
theme: null,
weekStart: "Sunday",
brandColor: "",
darkBrandColor: "",
allowDynamicBooking: !users.some((user) => {
return !user.allowDynamicBooking;
}),
}
: {
name: user.name || user.username,
image: user.avatar,
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
};
const profile = {
name: user.name || user.username,
image: user.avatar,
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
};
const eventTypesWithHidden = isDynamicGroup ? [] : await getEventTypesWithHiddenFromDB(user.id);
const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id);
const dataFetchEnd = Date.now();
if (context.query.log === "1") {
context.res.setHeader("X-Data-Fetch-Time", `${dataFetchEnd - dataFetchStart}ms`);
@@ -353,11 +281,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}));
const isSingleUser = users.length === 1;
const dynamicUsernames = isDynamicGroup
? users.map((user) => {
return user.username || "";
})
: [];
const safeBio = markdownToSafeHTML(user.bio) || "";
@@ -369,20 +292,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
safeBio,
profile,
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: isDynamicGroup ? null : user.username,
themeBasis: user.username,
user: {
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
},
eventTypes: isDynamicGroup
? defaultEvents.map((event) => {
event.description = getDynamicEventDescription(dynamicUsernames, event.slug);
return event;
})
: eventTypes,
eventTypes,
trpcState: ssr.dehydrate(),
isDynamicGroup,
dynamicNames,
dynamicUsernames,
isSingleUser,
markdownStrippedBio,
},
+150 -318
View File
@@ -1,97 +1,70 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import type { LocationObject } from "@calcom/app-store/locations";
import { Booker } from "@calcom/atoms";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { User } from "@calcom/prisma/client";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
import { ssrInit } from "@server/lib/ssr";
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export type AvailabilityPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export default function Type(props: AvailabilityPageProps) {
const { t } = useLocale();
return props.away ? (
<div className="dark:bg-inverted h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
😴{" " + t("user_away")}
</h2>
<p className="mx-auto max-w-md">{t("user_away_description")}</p>
</div>
</div>
</div>
</main>
</div>
) : props.isDynamic && !props.profile.allowDynamicBooking ? (
<div className="dark:bg-darkgray-50 h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
{" " + t("unavailable")}
</h2>
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled")}</p>
</div>
</div>
</div>
</main>
</div>
) : !props.isValidOrgDomain && props.organizationContext ? (
<div className="dark:bg-darkgray-50 h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
{" " + t("unavailable")}
</h2>
<p className="mx-auto max-w-md">{t("user_belongs_organization")}</p>
</div>
</div>
</div>
</main>
</div>
) : (
<AvailabilityPage {...props} />
export default function Type({
slug,
user,
isEmbed,
booking,
away,
isBrandingHidden,
rescheduleUid,
org,
}: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={rescheduleUid ?? undefined}
hideBranding={isBrandingHidden}
org={org}
/>
<Booker
username={user}
eventSlug={slug}
bookingData={booking}
isAway={away}
hideBranding={isBrandingHidden}
org={org}
/>
</main>
);
}
Type.isBookingPage = true;
Type.PageWrapper = PageWrapper;
const paramsSchema = z.object({ type: z.string(), user: z.string() });
async function getUserPageProps(context: GetServerSidePropsContext) {
// load server side dependencies
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
const { EventTypeMetaDataSchema, teamMetadataSchema } = await import("@calcom/prisma/zod-utils");
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const ssr = await ssrInit(context);
const { type: slug, user: username } = paramsSchema.parse(context.query);
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const { rescheduleUid, bookingUid } = context.query;
const user = await prisma.user.findFirst({
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const users = await prisma.user.findMany({
where: {
/** TODO: We should standarize this */
username: username.toLowerCase().replace(/( |%20)/g, "+"),
username: {
in: usernames,
},
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
@@ -99,183 +72,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
: null,
},
select: {
id: true,
username: true,
away: true,
name: true,
hideBranding: true,
timeZone: true,
theme: true,
weekStart: true,
brandColor: true,
darkBrandColor: true,
metadata: true,
organizationId: true,
eventTypes: {
where: {
// Many-to-many relationship causes inclusion of the team events - cool -
// but to prevent these from being selected, make sure the teamId is NULL.
AND: [{ slug }, { teamId: null }],
},
select: {
title: true,
slug: true,
hidden: true,
recurringEvent: true,
length: true,
locations: true,
id: true,
description: true,
price: true,
currency: true,
requiresConfirmation: true,
schedulingType: true,
metadata: true,
seatsPerTimeSlot: true,
team: {
select: {
logo: true,
parent: {
select: {
logo: true,
name: true,
},
},
},
},
},
orderBy: [
{
position: "desc",
},
{
id: "asc",
},
],
},
teams: {
include: {
team: true,
},
},
},
});
if (!user || !user.eventTypes.length) return { notFound: true };
const [eventType]: ((typeof user.eventTypes)[number] & {
users: Pick<User, "name" | "username" | "hideBranding" | "timeZone">[];
})[] = [
{
...user.eventTypes[0],
users: [
{
name: user.name,
username: user.username,
hideBranding: user.hideBranding,
timeZone: user.timeZone,
},
],
},
];
if (!eventType) return { notFound: true };
//TODO: Use zodSchema to verify it instead of using Type Assertion
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
const eventTypeObject = Object.assign({}, eventType, {
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: privacyFilteredLocations(locations),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
});
// Check if the user you are logging into has any active teams or premium user name
const hasActiveTeam =
user.teams.filter((m) => {
if (!IS_TEAM_BILLING_ENABLED) return true;
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return true;
return false;
}).length > 0;
const hasPremiumUserName = hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false;
return {
props: {
eventType: eventTypeObject,
profile: {
...eventType.users[0],
theme: user.theme,
allowDynamicBooking: false,
weekStart: user.weekStart,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
slug: `${user.username}/${eventType.slug}`,
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
},
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: user.username,
organizationContext: user?.organizationId !== null,
away: user?.away,
isDynamic: false,
trpcState: ssr.dehydrate(),
isValidOrgDomain: orgDomainConfig(context.req.headers.host ?? ""),
isBrandingHidden: isBrandingHidden(user.hideBranding, hasActiveTeam || hasPremiumUserName),
},
};
}
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
// load server side dependencies
const { getDefaultEvent, getGroupName, getUsernameList } = await import("@calcom/lib/defaultEvents");
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const { EventTypeMetaDataSchema, userMetadata: userMetadataSchema } = await import(
"@calcom/prisma/zod-utils"
);
const ssr = await ssrInit(context);
const { getAppFromSlug } = await import("@calcom/app-store/utils");
const { type: typeParam, user: userParam } = paramsSchema.parse(context.params);
const usernameList = getUsernameList(userParam);
const length = parseInt(typeParam);
const eventType = getDefaultEvent("" + length);
const users = await prisma.user.findMany({
where: {
username: {
in: usernameList,
},
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
startTime: true,
endTime: true,
timeZone: true,
weekStart: true,
availability: true,
hideBranding: true,
brandColor: true,
darkBrandColor: true,
defaultScheduleId: true,
allowDynamicBooking: true,
metadata: true,
organizationId: true,
away: true,
schedules: {
select: {
availability: true,
timeZone: true,
id: true,
},
},
theme: true,
},
});
@@ -284,87 +81,122 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
notFound: true,
};
}
const org = isValidOrgDomain ? currentOrgDomain : null;
// sort and be in the same order as usernameList so first user is the first user in the list
let sortedUsers: typeof users = [];
if (users.length > 1) {
sortedUsers = users.sort((a, b) => {
const aIndex = (a.username && usernameList.indexOf(a.username)) || 0;
const bIndex = (b.username && usernameList.indexOf(b.username)) || 0;
return aIndex - bIndex;
});
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingForReschedule(`${rescheduleUid}`);
} else if (bookingUid) {
booking = await getBookingForSeatedEvent(`${bookingUid}`);
}
let locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({
username: usernames.join("+"),
eventSlug: slug,
org,
});
// Get the prefered location type from the first user
const firstUsersMetadata = userMetadataSchema.parse(sortedUsers[0].metadata || {});
const preferedLocationType = firstUsersMetadata?.defaultConferencingApp;
if (preferedLocationType?.appSlug) {
const foundApp = getAppFromSlug(preferedLocationType.appSlug);
const appType = foundApp?.appData?.location?.type;
if (appType) {
// Replace the location with the prefered location type
// This will still be default to daily if the app is not found
locations = [{ type: appType, link: preferedLocationType.appLink }] as LocationObject[];
}
if (!eventData) {
return {
notFound: true,
};
}
const eventTypeObject = Object.assign({}, eventType, {
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: privacyFilteredLocations(locations),
users: users.map((user) => {
return {
name: user.name,
username: user.username,
hideBranding: user.hideBranding,
timeZone: user.timeZone,
};
}),
});
const dynamicNames = users.map((user) => {
return user.name || "";
});
const profile = {
name: getGroupName(dynamicNames),
image: null,
slug: "" + length,
theme: null as string | null,
weekStart: "Sunday",
brandColor: "",
darkBrandColor: "",
allowDynamicBooking: !users.some((user) => {
return !user.allowDynamicBooking;
}),
};
return {
props: {
eventType: eventTypeObject,
profile,
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: null,
isDynamic: true,
org,
booking,
user: usernames.join("+"),
slug,
away: false,
organizationContext: !users.some((user) => user.organizationId === null),
trpcState: ssr.dehydrate(),
isValidOrgDomain: orgDomainConfig(context.req.headers.host ?? ""),
isBrandingHidden: false, // I think we should always show branding for dynamic groups - saves us checking every single user
isBrandingHidden: false,
themeBasis: null,
bookingUid: bookingUid ? `${bookingUid}` : null,
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
},
};
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { user: userParam } = paramsSchema.parse(context.params);
// dynamic groups are not generated at build time, but otherwise are probably cached until infinity.
const isDynamicGroup = getUsernameList(userParam).length > 1;
if (isDynamicGroup) {
return await getDynamicGroupPageProps(context);
} else {
return await getUserPageProps(context);
async function getUserPageProps(context: GetServerSidePropsContext) {
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const username = usernames[0];
const { rescheduleUid, bookingUid } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
},
select: {
away: true,
hideBranding: true,
},
});
if (!user) {
return {
notFound: true,
};
}
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingForReschedule(`${rescheduleUid}`);
} else if (bookingUid) {
booking = await getBookingForSeatedEvent(`${bookingUid}`);
}
const org = isValidOrgDomain ? currentOrgDomain : null;
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({
username,
eventSlug: slug,
org,
});
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
booking,
away: user?.away,
user: username,
slug,
org,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
themeBasis: username,
bookingUid: bookingUid ? `${bookingUid}` : null,
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
},
};
}
const paramsSchema = z.object({
type: z.string().transform((s) => slugify(s)),
user: z.string().transform((s) => getUsernameList(s)),
});
// Booker page fetches a tiny bit of data server side, to determine early
// whether the page should show an away state or dynamic booking not allowed.
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { user } = paramsSchema.parse(context.params);
const isDynamicGroup = user.length > 1;
return isDynamicGroup ? await getDynamicGroupPageProps(context) : await getUserPageProps(context);
};
+6 -14
View File
@@ -1,19 +1,11 @@
import type { GetServerSidePropsContext } from "next";
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[type]";
export { default } from "../[type]";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssrResponse = await _getServerSideProps(context);
if (ssrResponse.notFound) {
return ssrResponse;
}
return {
...ssrResponse,
props: {
...ssrResponse.props,
isEmbed: true,
},
};
};
// Somehow these types don't accept the {notFound: true} return type.
// Probably still need to fix this. I don't know why this isn't allowed yet.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
-309
View File
@@ -1,309 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import type { LocationObject } from "@calcom/app-store/locations";
import { privacyFilteredLocations } from "@calcom/app-store/locations";
import { getAppFromSlug } from "@calcom/app-store/utils";
import dayjs from "@calcom/dayjs";
import getBooking from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { parseRecurringEvent } from "@calcom/lib";
import {
getDefaultEvent,
getDynamicEventName,
getGroupName,
getUsernameList,
} from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
import {
customInputSchema,
EventTypeMetaDataSchema,
userMetadata as userMetadataSchema,
} from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
import BookingPage from "@components/booking/pages/BookingPage";
import { ssrInit } from "@server/lib/ssr";
export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: BookPageProps) {
const { t } = useLocale();
return props.away ? (
<div className="dark:bg-inverted h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-default mb-2 text-3xl">
😴{" " + t("user_away")}
</h2>
<p className="mx-auto max-w-md">{t("user_away_description")}</p>
</div>
</div>
</div>
</main>
</div>
) : props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? (
<div className="dark:bg-inverted h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-default mb-2 text-3xl">
{" " + t("unavailable")}
</h2>
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled")}</p>
</div>
</div>
</div>
</main>
</div>
) : (
<BookingPage {...props} />
);
}
Book.isBookingPage = true;
Book.PageWrapper = PageWrapper;
const querySchema = z.object({
bookingUid: z.string().optional(),
count: z.coerce.number().optional(),
embed: z.string().optional(),
rescheduleUid: z.string().optional(),
slug: z.string().optional(),
/** This is the event "type" ID */
type: z.coerce.number().optional(),
user: z.string(),
seatReferenceUid: z.string().optional(),
date: z.string().optional(),
duration: z.coerce.number().optional(),
});
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const query = querySchema.parse(context.query);
const usernameList = getUsernameList(query.user);
const eventTypeSlug = query.slug;
const recurringEventCountQuery = query.count;
const users = await prisma.user.findMany({
where: {
username: {
in: usernameList,
},
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
theme: true,
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
away: true,
metadata: true,
},
});
if (!users.length) return { notFound: true };
const [user] = users;
const isDynamicGroupBooking = users.length > 1 && !!eventTypeSlug;
// Dynamic Group link doesn't need a type but it must have a slug
if ((!isDynamicGroupBooking && !query.type) || (users.length > 1 && !eventTypeSlug)) {
return { notFound: true };
}
const eventTypeRaw = isDynamicGroupBooking
? getDefaultEvent(eventTypeSlug)
: await prisma.eventType.findUnique({
where: {
id: query.type,
},
select: {
...bookEventTypeSelect,
},
});
if (!eventTypeRaw) return { notFound: true };
const eventType = {
...eventTypeRaw,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
bookingFields: getBookingFieldsWithSystemFields(eventTypeRaw),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
};
const getLocations = () => {
let locations = eventTypeRaw.locations || [];
if (!isDynamicGroupBooking) return locations;
let sortedUsers: typeof users = [];
// sort and be in the same order as usernameList so first user is the first user in the list
if (users.length > 1) {
sortedUsers = users.sort((a, b) => {
const aIndex = (a.username && usernameList.indexOf(a.username)) || 0;
const bIndex = (b.username && usernameList.indexOf(b.username)) || 0;
return aIndex - bIndex;
});
}
// Get the prefered location type from the first user
const firstUsersMetadata = userMetadataSchema.parse(sortedUsers[0].metadata || {});
const preferedLocationType = firstUsersMetadata?.defaultConferencingApp;
if (preferedLocationType?.appSlug) {
const foundApp = getAppFromSlug(preferedLocationType.appSlug);
const appType = foundApp?.appData?.location?.type;
if (appType) {
// Replace the location with the prefered location type
// This will still be default to daily if the app is not found
locations = [{ type: appType, link: preferedLocationType.appLink }] as LocationObject[];
}
}
return locations;
};
const eventTypeObject = [eventType].map((e) => {
let locations = getLocations();
locations = privacyFilteredLocations(locations as LocationObject[]);
return {
...e,
locations: locations,
periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null,
schedulingType: null,
customInputs: customInputSchema.array().parse(e.customInputs || []),
users: users.map((u) => ({
id: u.id,
name: u.name,
username: u.username,
avatar: u.avatar,
image: u.avatar,
slug: u.username,
theme: u.theme,
})),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
};
})[0];
// If rescheduleUid and event has seats lets convert Uid to bookingUid
let rescheduleUid = query.rescheduleUid;
const rescheduleEventTypeHasSeats = query.rescheduleUid && eventTypeRaw.seatsPerTimeSlot;
let attendeeEmail: string;
let bookingUidWithSeats: string | null = null;
if (rescheduleEventTypeHasSeats) {
const bookingSeat = await prisma.bookingSeat.findFirst({
where: {
referenceUid: query.rescheduleUid,
},
select: {
id: true,
attendee: true,
booking: {
select: {
uid: true,
},
},
},
});
if (bookingSeat) {
rescheduleUid = bookingSeat.booking.uid;
attendeeEmail = bookingSeat.attendee.email;
}
}
if (query.duration) {
// If it's not reschedule but event Type has seats we should obtain
// the bookingUid regardless and use it to get the booking
const currentSeats = await prisma.booking.findFirst({
where: {
eventTypeId: eventTypeRaw.id,
startTime: dayjs(query.date).toISOString(),
endTime: dayjs(query.date).add(query.duration, "minutes").toISOString(),
},
select: {
uid: true,
},
});
if (currentSeats && currentSeats) {
bookingUidWithSeats = currentSeats.uid;
}
}
let booking: GetBookingType | null = null;
if (rescheduleUid || query.bookingUid || bookingUidWithSeats) {
booking = await getBooking(prisma, rescheduleUid || query.bookingUid || bookingUidWithSeats || "");
}
if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) {
const currentAttendee = booking?.attendees.find((attendee) => {
return attendee.email === attendeeEmail;
});
if (currentAttendee) {
booking.attendees = [currentAttendee] || [];
}
}
const dynamicNames = isDynamicGroupBooking ? users.map((user) => user.name || "") : [];
const profile = isDynamicGroupBooking
? {
name: getGroupName(dynamicNames),
image: null,
slug: eventTypeSlug,
theme: null,
brandColor: "",
darkBrandColor: "",
allowDynamicBooking: !users.some((user) => !user.allowDynamicBooking),
eventName: getDynamicEventName(dynamicNames, eventTypeSlug),
}
: {
name: user.name || user.username,
image: user.avatar,
slug: user.username,
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
eventName: null,
};
// Checking if number of recurring event ocurrances is valid against event type configuration
const recurringEventCount =
(eventType.recurringEvent?.count &&
recurringEventCountQuery &&
(recurringEventCountQuery <= eventType.recurringEvent.count
? recurringEventCountQuery
: eventType.recurringEvent.count)) ||
null;
const currentSlotBooking = await getBooking(prisma, bookingUidWithSeats || "");
return {
props: {
away: user.away,
profile,
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: isDynamicGroupBooking ? null : user.username,
eventType: eventTypeObject,
booking,
currentSlotBooking: currentSlotBooking,
recurringEventCount,
trpcState: ssr.dehydrate(),
isDynamicGroupBooking,
hasHashedBookingLink: false,
hashedLink: null,
isEmbed: !!query.embed,
},
};
}
+11 -3
View File
@@ -28,17 +28,18 @@ class MyDocument extends Document<Props> {
// Use a dummy URL as default so that URL parsing works for relative URLs as well. We care about searchParams and pathname only
const parsedUrl = new URL(asPath, "https://dummyurl");
const isEmbed = parsedUrl.pathname.endsWith("/embed") || parsedUrl.searchParams.get("embedType") !== null;
const embedColorScheme = parsedUrl.searchParams.get("ui.color-scheme");
const initialProps = await Document.getInitialProps(ctx);
return { isEmbed, nonce, ...initialProps };
return { isEmbed, embedColorScheme, nonce, ...initialProps };
}
render() {
const { locale } = this.props.__NEXT_DATA__;
const { isEmbed } = this.props;
const { isEmbed, embedColorScheme } = this.props;
const nonceParsed = z.string().safeParse(this.props.nonce);
const nonce = nonceParsed.success ? nonceParsed.data : "";
return (
<Html lang={locale}>
<Html lang={locale} style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}>
<Head nonce={nonce}>
<link rel="apple-touch-icon" sizes="180x180" href="/api/logo?type=apple-touch-icon" />
<link rel="icon" type="image/png" sizes="32x32" href="/api/logo?type=favicon-32" />
@@ -48,6 +49,13 @@ class MyDocument extends Document<Props> {
<meta name="msapplication-TileColor" content="#ff0000" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
{(process.env.NODE_ENV === "development" || 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"
/>
)}
</Head>
<body
+6 -13
View File
@@ -11,8 +11,6 @@ import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const t = await getTranslation(req.body.language ?? "en", "common");
const email = z
.string()
.email()
@@ -47,6 +45,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
name: true,
identityProvider: true,
email: true,
locale: true,
},
});
@@ -57,6 +56,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
.json({ message: "If this email exists in our system, you should receive a Reset email." });
}
const t = await getTranslation(maybeUser.locale ?? "en", "common");
const maybePreviousRequest = await prisma.resetPasswordRequest.findMany({
where: {
email: maybeUser.email,
@@ -88,17 +89,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
resetLink,
});
/** So we can test the password reset flow on CI */
if (process.env.NEXT_PUBLIC_IS_E2E) {
return res.status(201).json({
message: "If this email exists in our system, you should receive a Reset email.",
resetLink,
});
} else {
return res
.status(201)
.json({ message: "If this email exists in our system, you should receive a Reset email." });
}
return res
.status(201)
.json({ message: "If this email exists in our system, you should receive a Reset email." });
} catch (reason) {
// console.error(reason);
return res.status(500).json({ message: "Unable to create password reset request" });
+49 -38
View File
@@ -1,55 +1,66 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { validPassword } from "@calcom/features/auth/lib/validPassword";
import prisma from "@calcom/prisma";
const passwordResetRequestSchema = z.object({
password: z.string().refine(validPassword, () => ({
message: "Password does not meet the requirements",
})),
requestId: z.string(), // format doesn't matter.
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(400).json({ message: "" });
}
// Bad Method when not POST
if (req.method !== "POST") return res.status(405).end();
const { password: rawPassword, requestId: rawRequestId } = passwordResetRequestSchema.parse(req.body);
// rate-limited there is a low, very low chance that a password request stays valid long enough
// to brute force 3.8126967e+40 options.
const maybeRequest = await prisma.resetPasswordRequest.findFirstOrThrow({
where: {
id: rawRequestId,
expires: {
gt: new Date(),
},
},
select: {
email: true,
},
});
const hashedPassword = await hashPassword(rawPassword);
// this can fail if a password request has been made for an email that has since changed or-
// never existed within Cal. In this case we do not want to disclose the email's existence.
// instead, we just return 404
try {
const rawPassword = req.body?.password;
const rawRequestId = req.body?.requestId;
if (!rawPassword || !rawRequestId) {
return res.status(400).json({ message: "Couldn't find an account for this email" });
}
const maybeRequest = await prisma.resetPasswordRequest.findUnique({
where: {
id: rawRequestId,
},
});
if (!maybeRequest) {
return res.status(400).json({ message: "Couldn't find an account for this email" });
}
const maybeUser = await prisma.user.findUnique({
where: {
email: maybeRequest.email,
},
});
if (!maybeUser) {
return res.status(400).json({ message: "Couldn't find an account for this email" });
}
const hashedPassword = await hashPassword(rawPassword);
await prisma.user.update({
where: {
id: maybeUser.id,
email: maybeRequest.email,
},
data: {
password: hashedPassword,
},
});
return res.status(201).json({ message: "Password reset." });
} catch (reason) {
console.error(reason);
return res.status(500).json({ message: "Unable to create password reset request" });
} catch (e) {
return res.status(404).end();
}
await expireResetPasswordRequest(rawRequestId);
return res.status(201).json({ message: "Password reset." });
}
async function expireResetPasswordRequest(rawRequestId: string) {
await prisma.resetPasswordRequest.update({
where: {
id: rawRequestId,
},
data: {
// We set the expiry to now to invalidate the request
expires: new Date(),
},
});
}
+170 -117
View File
@@ -2,16 +2,21 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
import { IS_CALCOM } from "@calcom/lib/constants";
import slugify from "@calcom/lib/slugify";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import { validateUsernameInOrg } from "@calcom/lib/validateUsernameInOrg";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
const signupSchema = z.object({
username: z.string(),
username: z.string().refine((value) => !value.includes("+"), {
message: "String should not contain a plus symbol (+).",
}),
email: z.string().email(),
password: z.string().min(7),
language: z.string().optional(),
@@ -20,7 +25,7 @@ const signupSchema = z.object({
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return;
return res.status(405).end();
}
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
@@ -39,60 +44,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return;
}
// There is an existingUser if the username matches
// OR if the email matches AND either the email is verified
// or both username and password are set
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ username },
{
AND: [
{ email: userEmail },
{
OR: [
{ emailVerified: { not: null } },
{
AND: [{ password: { not: null } }, { username: { not: null } }],
},
],
},
],
},
],
},
});
if (existingUser) {
const message: string =
existingUser.email !== userEmail ? "Username already taken" : "Email address is already registered";
return res.status(409).json({ message });
}
const hashedPassword = await hashPassword(password);
const user = await prisma.user.upsert({
where: { email: userEmail },
update: {
username,
password: hashedPassword,
emailVerified: new Date(Date.now()),
identityProvider: IdentityProvider.CAL,
},
create: {
username,
email: userEmail,
password: hashedPassword,
identityProvider: IdentityProvider.CAL,
},
});
let foundToken: { id: number; teamId: number | null; expires: Date } | null = null;
if (token) {
const foundToken = await prisma.verificationToken.findFirst({
foundToken = await prisma.verificationToken.findFirst({
where: {
token,
},
select: {
id: true,
expires: true,
teamId: true,
},
});
if (!foundToken) {
@@ -102,77 +64,144 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (dayjs(foundToken?.expires).isBefore(dayjs())) {
return res.status(401).json({ message: "Token expired" });
}
if (foundToken?.teamId) {
const isValidUsername = await validateUsernameInOrg(username, foundToken?.teamId);
if (foundToken.teamId) {
const team = await prisma.team.findUnique({
where: {
id: foundToken.teamId,
if (!isValidUsername) {
return res.status(409).json({ message: "Username already taken" });
}
}
} else {
// There is an existingUser if the username matches
// OR if the email matches AND either the email is verified
// or both username and password are set
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ username },
{
AND: [
{ email: userEmail },
{
OR: [
{ emailVerified: { not: null } },
{
AND: [{ password: { not: null } }, { username: { not: null } }],
},
],
},
],
},
],
},
});
if (existingUser) {
const message: string =
existingUser.email !== userEmail ? "Username already taken" : "Email address is already registered";
return res.status(409).json({ message });
}
}
const hashedPassword = await hashPassword(password);
if (foundToken && foundToken?.teamId) {
const team = await prisma.team.findUnique({
where: {
id: foundToken.teamId,
},
});
if (team) {
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
if (IS_CALCOM && (!teamMetadata?.isOrganization || !!team.parentId)) {
const checkUsername = await checkPremiumUsername(username);
if (checkUsername.premium) {
// This signup page is ONLY meant for team invites and local setup. Not for every day users.
// In singup redesign/refactor coming up @sean will tackle this to make them the same API/page instead of two.
return res.status(422).json({
message: "Sign up from https://cal.com/signup to claim your premium username",
});
}
}
const user = await prisma.user.upsert({
where: { email: userEmail },
update: {
username,
password: hashedPassword,
emailVerified: new Date(Date.now()),
identityProvider: IdentityProvider.CAL,
},
create: {
username,
email: userEmail,
password: hashedPassword,
identityProvider: IdentityProvider.CAL,
},
});
if (team) {
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
if (teamMetadata?.isOrganization) {
await prisma.user.update({
where: {
id: user.id,
},
data: {
organizationId: team.id,
},
});
}
const membership = await prisma.membership.update({
if (teamMetadata?.isOrganization) {
await prisma.user.update({
where: {
userId_teamId: { userId: user.id, teamId: team.id },
id: user.id,
},
data: {
organizationId: team.id,
},
});
}
const membership = await prisma.membership.update({
where: {
userId_teamId: { userId: user.id, teamId: team.id },
},
data: {
accepted: true,
},
});
closeComUpsertTeamUser(team, user, membership.role);
// Accept any child team invites for orgs.
if (team.parentId) {
// Join ORG
await prisma.user.update({
where: {
id: user.id,
},
data: {
organizationId: team.parentId,
},
});
/** We do a membership update twice so we can join the ORG invite if the user is invited to a team witin a ORG. */
await prisma.membership.updateMany({
where: {
userId: user.id,
team: {
id: team.parentId,
},
accepted: false,
},
data: {
accepted: true,
},
});
closeComUpsertTeamUser(team, user, membership.role);
// Accept any child team invites for orgs.
if (team.parentId) {
// Join ORG
await prisma.user.update({
where: {
id: user.id,
// Join any other invites
await prisma.membership.updateMany({
where: {
userId: user.id,
team: {
parentId: team.parentId,
},
data: {
organizationId: team.parentId,
},
});
/** We do a membership update twice so we can join the ORG invite if the user is invited to a team witin a ORG. */
await prisma.membership.updateMany({
where: {
userId: user.id,
team: {
id: team.parentId,
},
accepted: false,
},
data: {
accepted: true,
},
});
// Join any other invites
await prisma.membership.updateMany({
where: {
userId: user.id,
team: {
parentId: team.parentId,
},
accepted: false,
},
data: {
accepted: true,
},
});
}
accepted: false,
},
data: {
accepted: true,
},
});
}
}
@@ -182,13 +211,37 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: foundToken.id,
},
});
} else {
if (IS_CALCOM) {
const checkUsername = await checkPremiumUsername(username);
if (checkUsername.premium) {
res.status(422).json({
message: "Sign up from https://cal.com/signup to claim your premium username",
});
return;
}
}
await prisma.user.upsert({
where: { email: userEmail },
update: {
username,
password: hashedPassword,
emailVerified: new Date(Date.now()),
identityProvider: IdentityProvider.CAL,
},
create: {
username,
email: userEmail,
password: hashedPassword,
identityProvider: IdentityProvider.CAL,
},
});
await sendEmailVerification({
email: userEmail,
username,
language,
});
}
await sendEmailVerification({
email: userEmail,
username,
language,
});
res.status(201).json({ message: "Created user" });
}
@@ -6,6 +6,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/client";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
@@ -28,7 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(401).json({ message: "Not authenticated" });
}
if (!user.password) {
if (!user.password && user.identityProvider === IdentityProvider.CAL) {
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
}
@@ -36,9 +37,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.json({ message: "Two factor disabled" });
}
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
if (!isCorrectPassword) {
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
if (user.password && user.identityProvider === IdentityProvider.CAL) {
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
if (!isCorrectPassword) {
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
}
}
// if user has 2fa
if (user.twoFactorEnabled) {
@@ -0,0 +1,59 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
import type { BookingResponse, RecurringBookingCreateBody } from "@calcom/features/bookings/types";
import { defaultResponder } from "@calcom/lib/server";
import type { AppsStatus } from "@calcom/types/Calendar";
// @TODO: Didn't look at the contents of this function in order to not break old booking page.
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
const data: RecurringBookingCreateBody[] = req.body;
const session = await getServerSession({ req, res });
const createdBookings: BookingResponse[] = [];
const allRecurringDates: string[] = data.map((booking) => booking.start);
let appsStatus: AppsStatus[] | undefined = undefined;
/* To mimic API behavior and comply with types */
req.userId = session?.user?.id || -1;
// Reversing to accumulate results for noEmail instances first, to then lastly, create the
// emailed booking taking into account accumulated results to send app status accurately
for (let key = data.length - 1; key >= 0; key--) {
const booking = data[key];
if (key === 0) {
const calcAppsStatus: { [key: string]: AppsStatus } = createdBookings
.flatMap((book) => (book.appsStatus !== undefined ? book.appsStatus : []))
.reduce((prev, curr) => {
if (prev[curr.type]) {
prev[curr.type].failures += curr.failures;
prev[curr.type].success += curr.success;
} else {
prev[curr.type] = curr;
}
return prev;
}, {} as { [key: string]: AppsStatus });
appsStatus = Object.values(calcAppsStatus);
}
const recurringEventReq: NextApiRequest & { userId?: number } = req;
recurringEventReq.body = {
...booking,
appsStatus,
allRecurringDates,
currentRecurringIndex: key,
noEmail: key !== 0,
};
const eachRecurringBooking = await handleNewBooking(recurringEventReq, {
isNotAnApiCall: true,
});
createdBookings.push(eachRecurringBooking);
}
return createdBookings;
}
export default defaultResponder(handler);
@@ -0,0 +1 @@
export { default } from "@calcom/features/ee/workflows/api/scheduleWhatsappReminders";
+1 -1
View File
@@ -55,7 +55,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
],
};
req.statusCode = 200;
res.statusCode = 200;
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
+5 -1
View File
@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import type { Session } from "next-auth";
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
import { HttpError } from "@calcom/lib/http-error";
@@ -37,6 +38,9 @@ const defaultIntegrationAddHandler = async ({
throw new Error("App is already installed");
}
}
await throwIfNotHaveAdminAccessToTeam({ teamId: teamId ?? null, userId: user.id });
await createCredential({ user: user, appType, slug, teamId });
};
@@ -56,6 +60,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const handlerMap = (await import("@calcom/app-store/apps.server.generated")).apiHandlers;
const handlerKey = deriveAppDictKeyFromType(appName, handlerMap);
const handlers = await handlerMap[handlerKey as keyof typeof handlerMap];
if (!handlers) throw new HttpError({ statusCode: 404, message: `No handlers found for ${handlerKey}` });
const handler = handlers[apiEndpoint as keyof typeof handlers] as AppHandler;
let redirectUrl = "/apps/installed";
if (typeof handler === "undefined")
@@ -71,7 +76,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return res.status(200);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
return res.status(error.statusCode).json({ message: error.message });
}
-26
View File
@@ -1,26 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { defaultResponder } from "@calcom/lib/server";
const newBookerSchema = z.object({
status: z.enum(["enable", "disable"]),
});
/**
* Very basic temporary api route to enable/disable new booker access.
*/
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { status } = newBookerSchema.parse(req.query);
if (status === "enable") {
const expires = new Date();
expires.setFullYear(expires.getFullYear() + 1);
res.setHeader("Set-Cookie", `new-booker-enabled=true; path=/; expires=${expires.toUTCString()}`);
} else {
res.setHeader("Set-Cookie", "new-booker-enabled=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
}
res.send({ status: 200, body: `Done – ${status}` });
}
export default defaultResponder(handler);
+10 -1
View File
@@ -9,6 +9,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { defaultHandler } from "@calcom/lib/server";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
@@ -92,6 +93,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
eventType: {
select: {
teamId: true,
parentId: true,
},
},
user: {
@@ -168,13 +170,20 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
uid: booking.uid,
};
const teamId = await getTeamIdFromEventType({
eventType: {
team: { id: booking?.eventType?.teamId ?? null },
parentId: booking?.eventType?.parentId ?? null,
},
});
await triggerWebhook({
evt,
downloadLink,
booking: {
userId: booking?.user?.id,
eventTypeId: booking.eventTypeId,
teamId: booking.eventType?.teamId,
teamId,
},
});
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
@@ -22,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { code, state } = req.query;
const parsedState = stateSchema.parse(JSON.parse(state as string));
const { teamId } = parsedState;
await throwIfNotHaveAdminAccessToTeam({ teamId: Number(teamId) ?? null, userId: session.user.id });
if (code && typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
+4
View File
@@ -0,0 +1,4 @@
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
import { adminRouter } from "@calcom/trpc/server/routers/viewer/admin/_router";
export default createNextApiHandler(adminRouter);
+1 -1
View File
@@ -1,4 +1,4 @@
import { featureFlagRouter } from "@calcom/features/flags/server/router";
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
export default createNextApiHandler(featureFlagRouter);
export default createNextApiHandler(featureFlagRouter, true, "features");
+13 -1
View File
@@ -18,7 +18,7 @@ const querySchema = z
async function getIdentityData(req: NextApiRequest) {
const { username, teamname } = querySchema.parse(req.query);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
const org = isValidOrgDomain ? currentOrgDomain : null;
if (username) {
const user = await prisma.user.findFirst({
where: {
@@ -35,6 +35,7 @@ async function getIdentityData(req: NextApiRequest) {
name: username,
email: user?.email,
avatar: user?.avatar,
org,
};
}
if (teamname) {
@@ -50,6 +51,7 @@ async function getIdentityData(req: NextApiRequest) {
select: { logo: true },
});
return {
org,
name: teamname,
email: null,
avatar: team?.logo || getPlaceholderAvatar(null, teamname),
@@ -62,6 +64,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const img = identity?.avatar;
// If image isn't set or links to this route itself, use default avatar
if (!img) {
if (identity?.org) {
res.setHeader("x-cal-org", identity.org);
}
res.writeHead(302, {
Location: defaultAvatarSrc({
md5: crypto
@@ -70,16 +75,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
.digest("hex"),
}),
});
return res.end();
}
if (!img.includes("data:image")) {
if (identity.org) {
res.setHeader("x-cal-org", identity.org);
}
res.writeHead(302, { Location: img });
return res.end();
}
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
const imageResp = Buffer.from(decoded, "base64");
if (identity.org) {
res.setHeader("x-cal-org", identity.org);
}
res.writeHead(200, {
"Content-Type": "image/png",
"Content-Length": imageResp.length,
+1 -1
View File
@@ -46,7 +46,7 @@ function getRoute(appName: string, pages: string[]) {
} as NotFound;
}
const mainPage = pages[0];
const appPage = routingConfig[mainPage] as AppPageType;
const appPage = routingConfig.layoutHandler || (routingConfig[mainPage] as AppPageType);
if (!appPage) {
return {
+61 -97
View File
@@ -1,62 +1,42 @@
import type { ResetPasswordRequest } from "@prisma/client";
import { debounce } from "lodash";
import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Link from "next/link";
import type { CSSProperties } from "react";
import React, { useMemo } from "react";
import React from "react";
import { useForm } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { Button, TextField } from "@calcom/ui";
import { Button, PasswordField, Form } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
import AuthContainer from "@components/ui/AuthContainer";
type Props = {
id: string;
resetPasswordRequest: ResetPasswordRequest;
requestId: string;
isRequestExpired: boolean;
csrfToken: string;
};
export default function Page({ resetPasswordRequest, csrfToken }: Props) {
export default function Page({ requestId, isRequestExpired, csrfToken }: Props) {
const { t } = useLocale();
const [loading, setLoading] = React.useState(false);
const [, setError] = React.useState<{ message: string } | null>(null);
const [success, setSuccess] = React.useState(false);
const [password, setPassword] = React.useState("");
const formMethods = useForm<{ new_password: string }>();
const success = formMethods.formState.isSubmitSuccessful;
const loading = formMethods.formState.isSubmitting;
const submitChangePassword = async ({ password, requestId }: { password: string; requestId: string }) => {
try {
const res = await fetch("/api/auth/reset-password", {
method: "POST",
body: JSON.stringify({ requestId: requestId, password: password }),
headers: {
"Content-Type": "application/json",
},
});
const json = await res.json();
if (!res.ok) {
setError(json);
} else {
setSuccess(true);
}
return json;
} catch (reason) {
setError({ message: t("unexpected_error_try_again") });
} finally {
setLoading(false);
}
const res = await fetch("/api/auth/reset-password", {
method: "POST",
body: JSON.stringify({ requestId, password }),
headers: {
"Content-Type": "application/json",
},
});
const json = await res.json();
if (!res.ok) return formMethods.setError("new_password", { type: "server", message: json.message });
};
const debouncedChangePassword = debounce(submitChangePassword, 250);
const Success = () => {
return (
<>
@@ -95,11 +75,6 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
);
};
const isRequestExpired = useMemo(() => {
const now = dayjs();
return dayjs(resetPasswordRequest.expires).isBefore(now);
}, [resetPasswordRequest]);
return (
<AuthContainer
showLogo
@@ -109,8 +84,9 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
{isRequestExpired && <Expired />}
{!isRequestExpired && !success && (
<>
<form
<Form
className="space-y-6"
form={formMethods}
style={
{
"--cal-brand": "#111827",
@@ -119,36 +95,26 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
"--cal-brand-subtle": "#9CA3AF",
} as CSSProperties
}
onSubmit={async (e) => {
e.preventDefault();
if (!password) {
return;
}
if (loading) {
return;
}
setLoading(true);
setError(null);
setSuccess(false);
await debouncedChangePassword({ password, requestId: resetPasswordRequest.id });
}}
action="#">
handleSubmit={async (values) => {
await submitChangePassword({
password: values.new_password,
requestId,
});
}}>
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
<div className="mt-1">
<TextField
<PasswordField
{...formMethods.register("new_password", {
minLength: {
message: t("password_hint_min"),
value: 7, // We don't have user here so we can't check if they are admin or not
},
pattern: {
message: "Should contain a number, uppercase and lowercase letters",
value: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).*$/gm,
},
})}
label={t("new_password")}
onChange={(e) => {
setPassword(e.target.value);
}}
id="password"
name="password"
type="password"
autoComplete="password"
required
/>
</div>
@@ -162,7 +128,7 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
{t("reset_password")}
</Button>
</div>
</form>
</Form>
</>
)}
{!isRequestExpired && success && (
@@ -179,31 +145,29 @@ Page.PageWrapper = PageWrapper;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const id = context.params?.id as string;
let resetPasswordRequest = await prisma.resetPasswordRequest.findFirst({
where: {
id,
expires: {
gt: new Date(),
},
},
select: {
email: true,
},
});
try {
const resetPasswordRequest = await prisma.resetPasswordRequest.findUniqueOrThrow({
where: {
id,
},
select: {
id: true,
expires: true,
},
});
return {
props: {
resetPasswordRequest: {
...resetPasswordRequest,
expires: resetPasswordRequest.expires.toString(),
},
id,
csrfToken: await getCsrfToken({ req: context.req }),
...(await serverSideTranslations(context.locale || "en", ["common"])),
},
};
} catch (reason) {
return {
notFound: true,
};
resetPasswordRequest &&
(await prisma.user.findUniqueOrThrow({ where: { email: resetPasswordRequest.email } }));
} catch (e) {
resetPasswordRequest = null;
}
return {
props: {
isRequestExpired: !resetPasswordRequest,
requestId: id,
csrfToken: await getCsrfToken({ req: context.req }),
...(await serverSideTranslations(context.locale || "en", ["common"])),
},
};
}
@@ -15,7 +15,7 @@ import PageWrapper from "@components/PageWrapper";
import AuthContainer from "@components/ui/AuthContainer";
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
const { t, i18n } = useLocale();
const { t } = useLocale();
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<{ message: string } | null>(null);
const [success, setSuccess] = React.useState(false);
@@ -31,7 +31,7 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
try {
const res = await fetch("/api/auth/forgot-password", {
method: "POST",
body: JSON.stringify({ email: email, language: i18n.language }),
body: JSON.stringify({ email }),
headers: {
"Content-Type": "application/json",
},
+2 -2
View File
@@ -57,7 +57,7 @@ export default function Login({
.string()
.min(1, `${t("error_required_field")}`)
.email(`${t("enter_valid_email")}`),
password: z.string().min(1, `${t("error_required_field")}`),
password: !!totpEmail ? z.literal("") : z.string().min(1, `${t("error_required_field")}`),
})
// Passthrough other fields like totpCode
.passthrough();
@@ -69,7 +69,7 @@ export default function Login({
const errorMessages: { [key: string]: string } = {
// [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
// Don't leak information about whether an email is registered or not
[ErrorCode.IncorrectUsernamePassword]: t("incorrect_username_password"),
[ErrorCode.IncorrectEmailPassword]: t("incorrect_email_password"),
[ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`,
[ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
+11 -6
View File
@@ -3,11 +3,11 @@ import { useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
import Schedule from "@calcom/features/schedules/components/Schedule";
import Shell from "@calcom/features/shell/Shell";
import { availabilityAsString } from "@calcom/lib/availability";
import { yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
@@ -53,9 +53,10 @@ type AvailabilityFormValues = {
};
const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
const { remove, append, update, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
const { remove, append, replace, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
name: "dateOverrides",
});
const excludedDates = fields.map((field) => dayjs(field.ranges[0].start).utc().format("YYYY-MM-DD"));
const { t } = useLocale();
return (
<div className="p-6">
@@ -70,15 +71,15 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
<p className="text-subtle mb-4 text-sm">{t("date_overrides_subtitle")}</p>
<div className="space-y-2">
<DateOverrideList
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
excludedDates={excludedDates}
remove={remove}
update={update}
replace={replace}
items={fields}
workingHours={workingHours}
/>
<DateOverrideInputDialog
workingHours={workingHours}
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
excludedDates={excludedDates}
onChange={(ranges) => append({ ranges })}
Trigger={
<Button color="secondary" StartIcon={Plus} data-testid="add-override">
@@ -278,7 +279,11 @@ export default function Availability() {
</Dropdown>
<div className="border-default border-l-2" />
<Button className="ml-4 lg:ml-0" type="submit" form="availability-form">
<Button
className="ml-4 lg:ml-0"
type="submit"
form="availability-form"
loading={updateMutation.isLoading}>
{t("save")}
</Button>
</div>
+1
View File
@@ -95,6 +95,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
Icon={Clock}
headline={t("new_schedule_heading")}
description={t("new_schedule_description")}
className="w-full"
buttonRaw={<NewScheduleButton />}
/>
</div>
+7 -5
View File
@@ -23,9 +23,9 @@ import {
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { SystemField } from "@calcom/features/bookings/lib/SystemField";
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
import {
SystemField,
getBookingFieldsWithSystemFields,
SMS_REMINDER_NUMBER_FIELD,
} from "@calcom/features/bookings/lib/getBookingFields";
@@ -185,7 +185,7 @@ export default function Success(props: SuccessProps) {
// - It's a paid event and payment is pending.
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING && eventType.requiresConfirmation;
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
const isLoggedIn = session?.user;
const isCancelled =
status === "CANCELLED" ||
status === "REJECTED" ||
@@ -278,7 +278,7 @@ export default function Success(props: SuccessProps) {
// This is a weird case where the same route can be opened in booking flow as a success page or as a booking detail page from the app
// As Booking Page it has to support configured theme, but as booking detail page it should not do any change. Let Shell.tsx handle it.
useTheme(isSuccessBookingPage ? props.profile.theme : undefined);
useTheme(isSuccessBookingPage ? props.profile.theme : "system");
useBrandColors({
brandColor: props.profile.brandColor,
darkBrandColor: props.profile.darkBrandColor,
@@ -310,7 +310,7 @@ export default function Success(props: SuccessProps) {
status={status}
/>
)}
{userIsOwner && !isEmbed && (
{isLoggedIn && !isEmbed && (
<div className="-mb-4 ml-4 mt-2">
<Link
href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}
@@ -440,7 +440,9 @@ export default function Success(props: SuccessProps) {
{bookingInfo?.user && (
<div className="mb-3">
<div>
<span className="mr-2">{bookingInfo.user.name}</span>
<span data-testid={`host-name-${bookingInfo.user.name}`} className="mr-2">
{bookingInfo.user.name}
</span>
<Badge variant="blue">{t("Host")}</Badge>
</div>
<p className="text-default">{bookingInfo.user.email}</p>
+105 -126
View File
@@ -1,42 +1,65 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import type { LocationObject } from "@calcom/core/location";
import { privacyFilteredLocations } from "@calcom/core/location";
import { Booker } from "@calcom/atoms";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { parseRecurringEvent } from "@calcom/lib";
import { getWorkingHours } from "@calcom/lib/availability";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
import { ssrInit } from "@server/lib/ssr";
type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export type DynamicAvailabilityPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export default function Type(props: DynamicAvailabilityPageProps) {
return <AvailabilityPage {...props} />;
export default function Type({
slug,
isEmbed,
user,
booking,
away,
isBrandingHidden,
isTeamEvent,
org,
}: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
org={org}
/>
<Booker
username={user}
eventSlug={slug}
bookingData={booking}
isAway={away}
hideBranding={isBrandingHidden}
isTeamEvent={isTeamEvent}
org={org}
/>
</main>
);
}
Type.isBookingPage = true;
Type.PageWrapper = PageWrapper;
Type.isBookingPage = true;
const querySchema = z.object({
link: z.string().optional().default(""),
slug: z.string().optional().default(""),
date: z.union([z.string(), z.null()]).optional().default(null),
});
async function getUserPageProps(context: GetServerSidePropsContext) {
const { link, slug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const org = isValidOrgDomain ? currentOrgDomain : null;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const { link, slug, date } = querySchema.parse(context.query);
const hashedLink = await prisma.hashedLink.findUnique({
where: {
@@ -45,132 +68,88 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
select: {
eventTypeId: true,
eventType: {
select: availiblityPageEventTypeSelect,
},
},
});
const userId = hashedLink?.eventType.userId || hashedLink?.eventType.users[0]?.id;
if (!userId)
return {
notFound: true,
} as {
notFound: true;
};
if (hashedLink?.eventType.slug !== slug)
return {
notFound: true,
} as {
notFound: true;
};
const users = await prisma.user.findMany({
where: {
id: userId,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
startTime: true,
endTime: true,
timeZone: true,
weekStart: true,
availability: true,
hideBranding: true,
brandColor: true,
darkBrandColor: true,
defaultScheduleId: true,
allowDynamicBooking: true,
away: true,
schedules: {
select: {
availability: true,
timeZone: true,
id: true,
users: {
select: {
username: true,
},
},
team: {
select: {
id: true,
},
},
},
},
theme: true,
},
});
if (!users || !users.length) {
const username = hashedLink?.eventType.users[0]?.username;
if (!hashedLink || !username) {
return {
notFound: true,
} as {
notFound: true;
};
}
const locations = hashedLink.eventType.locations
? (hashedLink.eventType.locations as LocationObject[])
: [];
const eventTypeObject = Object.assign({}, hashedLink.eventType, {
metadata: EventTypeMetaDataSchema.parse(hashedLink.eventType.metadata || {}),
recurringEvent: parseRecurringEvent(hashedLink.eventType.recurringEvent),
periodStartDate: hashedLink.eventType.periodStartDate?.toString() ?? null,
periodEndDate: hashedLink.eventType.periodEndDate?.toString() ?? null,
slug,
locations: privacyFilteredLocations(locations),
users: users.map((u) => ({
name: u.name,
username: u.username,
hideBranding: u.hideBranding,
timeZone: u.timeZone,
})),
descriptionAsSafeHTML: markdownToSafeHTML(hashedLink.eventType.description),
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
},
select: {
away: true,
hideBranding: true,
},
});
const [user] = users;
if (!user) {
return {
notFound: true,
};
}
const schedule = {
...user.schedules.filter(
(schedule) => !user.defaultScheduleId || schedule.id === user.defaultScheduleId
)[0],
};
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingForReschedule(`${rescheduleUid}`);
}
const timeZone = schedule.timeZone || user.timeZone;
const isTeamEvent = !!hashedLink.eventType?.team?.id;
const workingHours = getWorkingHours(
{
timeZone,
},
schedule.availability || user.availability
);
eventTypeObject.schedule = null;
eventTypeObject.availability = [];
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug, isTeamEvent, org });
const booking: GetBookingType | null = null;
const profile = {
name: user.name || user.username,
image: user.avatar,
slug: user.username,
theme: user.theme,
weekStart: user.weekStart,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
};
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
away: user.away,
themeBasis: user.username,
isDynamicGroup: false,
profile,
date,
eventType: eventTypeObject,
workingHours,
trpcState: ssr.dehydrate(),
previousPage: context.req.headers.referer ?? null,
org,
booking,
users: [user.username],
isBrandingHidden: user.hideBranding,
away: user?.away,
user: username,
slug,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
// Sending the team event from the server, because this template file
// is reused for both team and user events.
isTeamEvent,
},
};
}
const paramsSchema = z.object({ link: z.string(), slug: z.string().transform((s) => slugify(s)) });
// Booker page fetches a tiny bit of data server side, to determine early
// whether the page should show an away state or dynamic booking not allowed.
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
return await getUserPageProps(context);
};
-7
View File
@@ -1,7 +0,0 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[slug]";
export { default } from "../[slug]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
-137
View File
@@ -1,137 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { parseRecurringEvent } from "@calcom/lib";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma from "@calcom/prisma";
import { bookEventTypeSelect } from "@calcom/prisma/selects";
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
import BookingPage from "@components/booking/pages/BookingPage";
import { ssrInit } from "@server/lib/ssr";
export type HashLinkPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: HashLinkPageProps) {
return <BookingPage {...props} />;
}
Book.isBookingPage = true;
Book.PageWrapper = PageWrapper;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const link = asStringOrThrow(context.query.link as string);
const recurringEventCountQuery = asStringOrNull(context.query.count);
const hashedLink = await prisma.hashedLink.findUnique({
where: {
link,
},
select: {
eventTypeId: true,
eventType: {
select: bookEventTypeSelect,
},
},
});
const userId = hashedLink?.eventType.userId || hashedLink?.eventType.users[0]?.id;
if (!userId)
return {
notFound: true,
};
const users = await prisma.user.findMany({
where: {
id: userId,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
theme: true,
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
},
});
if (!users.length) return { notFound: true };
const [user] = users;
const eventTypeRaw = hashedLink?.eventType;
if (!eventTypeRaw) return { notFound: true };
const eventType = {
...eventTypeRaw,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
bookingFields: eventTypeBookingFields.parse(eventTypeRaw.bookingFields || []),
};
const eventTypeObject = [eventType].map((e) => {
return {
...e,
periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null,
customInputs: customInputSchema.array().parse(e.customInputs || []),
schedulingType: null,
users: users.map((u) => ({
id: u.id,
name: u.name,
username: u.username,
avatar: u.avatar,
image: u.avatar,
slug: u.username,
theme: u.theme,
email: u.email,
brandColor: u.brandColor,
darkBrandColor: u.darkBrandColor,
})),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
};
})[0];
const profile = {
name: user.name || user.username,
image: user.avatar,
slug: user.username,
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
eventName: null,
};
// Checking if number of recurring event ocurrances is valid against event type configuration
const recurringEventCount =
(eventTypeObject.recurringEvent?.count &&
recurringEventCountQuery &&
(parseInt(recurringEventCountQuery) <= eventTypeObject.recurringEvent.count
? parseInt(recurringEventCountQuery)
: eventType.recurringEvent?.count)) ||
null;
return {
props: {
profile,
themeBasis: user.username,
eventType: eventTypeObject,
booking: null,
currentSlotBooking: null,
trpcState: ssr.dehydrate(),
recurringEventCount,
isDynamicGroupBooking: false,
hasHashedBookingLink: true,
hashedLink: link,
isEmbed: typeof context.query.embed === "string",
},
};
}
+2 -2
View File
@@ -503,7 +503,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
</Form>
</EventTypeSingleLayout>
{slugExistsChildrenDialogOpen.length && (
{slugExistsChildrenDialogOpen.length ? (
<ManagedEventTypeDialog
slugExistsChildrenDialogOpen={slugExistsChildrenDialogOpen}
isLoading={formMethods.formState.isSubmitting}
@@ -518,7 +518,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
setSlugExistsChildrenDialogOpen([]);
}}
/>
)}
) : null}
</>
);
};
+85 -73
View File
@@ -7,7 +7,7 @@ import type { FC } from "react";
import { useEffect, useState, memo } from "react";
import { z } from "zod";
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
@@ -16,6 +16,7 @@ import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import Shell from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
@@ -66,7 +67,6 @@ import {
User as UserIcon,
} from "@calcom/ui/components/icon";
import { withQuery } from "@lib/QueryCell";
import useMeQuery from "@lib/hooks/useMeQuery";
import { EmbedButton, EmbedDialog } from "@components/Embed";
@@ -103,11 +103,11 @@ const querySchema = z.object({
const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
const { eventTypeGroups } = props;
const orgBranding = useOrgBranding();
const tabs = eventTypeGroups.map((item) => ({
name: item.profile.name ?? "",
href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types",
avatar: item.profile.image ?? `${WEBAPP_URL}/${item.profile.slug}/avatar.png`,
avatar: item.profile.image ?? `${orgBranding?.fullDomain ?? WEBAPP_URL}/${item.profile.slug}/avatar.png`,
}));
const { data } = useTypedQuery(querySchema);
const events = eventTypeGroups.filter((item) => item.teamId === data.teamId);
@@ -201,7 +201,7 @@ const MemoizedItem = memo(Item);
export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => {
const { t } = useLocale();
const router = useRouter();
const orgBranding = useOrgBrandingValues();
const orgBranding = useOrgBranding();
const [parent] = useAutoAnimate<HTMLUListElement>();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
@@ -404,7 +404,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
items={type.users.map(
(organizer: { name: string | null; username: string | null }) => ({
alt: organizer.name || "",
image: `${WEBAPP_URL}/${organizer.username}/avatar.png`,
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${
organizer.username
}/avatar.png`,
title: organizer.name || "",
})
)}
@@ -419,7 +421,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
.flatMap((ch) => ch.users)
.map((user: Pick<User, "name" | "username">) => ({
alt: user.name || "",
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${user.username}/avatar.png`,
title: user.name || "",
}))}
/>
@@ -512,7 +514,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
type="button"
StartIcon={Code}
className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}>
embedUrl={encodeURIComponent(embedLink)}
eventId={type.id}>
{t("embed")}
</EmbedButton>
</DropdownMenuItem>
@@ -698,7 +701,7 @@ const EventTypeListHeading = ({
}: EventTypeListHeadingProps): JSX.Element => {
const { t } = useLocale();
const router = useRouter();
const orgBranding = useOrgBrandingValues();
const orgBranding = useOrgBranding();
const publishTeamMutation = trpc.viewer.teams.publish.useMutation({
onSuccess(data) {
@@ -708,13 +711,16 @@ const EventTypeListHeading = ({
showToast(error.message, "error");
},
});
const bookerUrl = useBookerUrl();
return (
<div className="mb-4 flex items-center space-x-2">
<Avatar
alt={profile?.name || ""}
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
imageSrc={`${WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
imageSrc={
`${orgBranding?.fullDomain ?? WEBAPP_URL}/${teamId ? "team/" : ""}${profile.slug}/avatar.png` ||
undefined
}
size="md"
className="mt-1 inline-flex justify-center"
/>
@@ -736,9 +742,7 @@ const EventTypeListHeading = ({
)}
{profile?.slug && (
<Link href={`${CAL_URL}/${profile.slug}`} className="text-subtle block text-xs">
{orgBranding
? `${orgBranding.fullDomain.replace("https://", "").replace("http://", "")}${profile.slug}`
: `${CAL_URL?.replace("https://", "").replace("http://", "")}/${profile.slug}`}
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
</Link>
)}
</div>
@@ -804,7 +808,7 @@ const Actions = () => {
const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
const { t } = useLocale();
const orgBranding = useOrgBrandingValues();
const orgBranding = useOrgBranding();
return (
<Alert
@@ -830,15 +834,78 @@ const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
);
};
const Main = () => {
const isMobile = useMediaQuery("(max-width: 768px)");
const router = useRouter();
const filters = getTeamsFiltersFromQuery(router.query);
const orgBranding = useOrgBranding();
// TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
const res = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters });
if (res.status === "loading") {
return <SkeletonLoader />;
}
if (res.status === "error") {
return <Alert severity="error" title="Something went wrong" message={res.error.message} />;
}
const data = res.data;
const isFilteredByOnlyOneItem =
(filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) && data.eventTypeGroups.length === 1;
return (
<>
{data.eventTypeGroups.length > 1 || isFilteredByOnlyOneItem ? (
<>
{isMobile ? (
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
) : (
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
<div className="flex flex-col" key={group.profile.slug}>
<EventTypeListHeading
profile={group.profile}
membershipCount={group.metadata.membershipCount}
teamId={group.teamId}
orgSlug={orgBranding?.slug}
/>
<EventTypeList
types={group.eventTypes}
group={group}
groupIndex={index}
readOnly={group.metadata.readOnly}
/>
</div>
))
)}
</>
) : (
data.eventTypeGroups.length === 1 && (
<EventTypeList
types={data.eventTypeGroups[0].eventTypes}
group={data.eventTypeGroups[0]}
groupIndex={0}
readOnly={data.eventTypeGroups[0].metadata.readOnly}
/>
)
)}
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView />}
<EmbedDialog />
{router.query.dialog === "duplicate" && <DuplicateDialog />}
</>
);
};
const EventTypesPage = () => {
const { t } = useLocale();
const router = useRouter();
const { open } = useIntercom();
const { query } = router;
const { data: user } = useMeQuery();
const isMobile = useMediaQuery("(max-width: 768px)");
const [showProfileBanner, setShowProfileBanner] = useState(false);
const orgBranding = useOrgBrandingValues();
const orgBranding = useOrgBranding();
function closeBanner() {
setShowProfileBanner(false);
@@ -855,10 +922,6 @@ const EventTypesPage = () => {
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const filters = getTeamsFiltersFromQuery(router.query);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer as any, filters && { filters });
return (
<div>
@@ -874,58 +937,7 @@ const EventTypesPage = () => {
afterHeading={showProfileBanner && <SetupProfileBanner closeAction={closeBanner} />}
beforeCTAactions={<Actions />}
CTA={<CTA />}>
<WithQuery
customLoader={<SkeletonLoader />}
success={({ data }) => {
const isFilteredByOnlyOneItem =
(filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) &&
data.eventTypeGroups.length === 1;
return (
<>
{data.eventTypeGroups.length > 1 || isFilteredByOnlyOneItem ? (
<>
{isMobile ? (
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
) : (
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
<div className="flex flex-col" key={group.profile.slug}>
<EventTypeListHeading
profile={group.profile}
membershipCount={group.metadata.membershipCount}
teamId={group.teamId}
orgSlug={orgBranding?.slug}
/>
<EventTypeList
types={group.eventTypes}
group={group}
groupIndex={index}
readOnly={group.metadata.readOnly}
/>
</div>
))
)}
</>
) : (
data.eventTypeGroups.length === 1 && (
<EventTypeList
types={data.eventTypeGroups[0].eventTypes}
group={data.eventTypeGroups[0]}
groupIndex={0}
readOnly={data.eventTypeGroups[0].metadata.readOnly}
/>
)
)}
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView />}
<EmbedDialog />
{router.query.dialog === "duplicate" && <DuplicateDialog />}
</>
);
}}
/>
<Main />
</Shell>
</div>
);
@@ -42,6 +42,7 @@ const stepTransform = (step: (typeof steps)[number]) => {
const stepRouteSchema = z.object({
step: z.array(z.enum(steps)).default([INITIAL_STEP]),
from: z.string().optional(),
});
// TODO: Refactor how steps work to be contained in one array/object. Currently we have steps,initalsteps,headers etc. These can all be in one place
@@ -52,6 +53,7 @@ const OnboardingPage = () => {
const result = stepRouteSchema.safeParse(router.query);
const currentStep = result.success ? result.data.step[0] : INITIAL_STEP;
const from = result.success ? result.data.from : "";
const headers = [
{
@@ -141,7 +143,9 @@ const OnboardingPage = () => {
</div>
<StepCard>
<Suspense fallback={<Loader />}>
{currentStep === "user-settings" && <UserSettings nextStep={() => goToIndex(1)} />}
{currentStep === "user-settings" && (
<UserSettings nextStep={() => goToIndex(1)} hideUsername={from === "signup"} />
)}
{currentStep === "connected-calendar" && <ConnectedCalendars nextStep={() => goToIndex(2)} />}
{currentStep === "connected-video" && <ConnectedVideoStep nextStep={() => goToIndex(3)} />}
-164
View File
@@ -1,164 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { Booker } from "@calcom/atoms";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { classNames } from "@calcom/lib";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
return (
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
/>
</main>
);
}
Type.PageWrapper = PageWrapper;
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const users = await prisma.user.findMany({
where: {
username: {
in: usernames,
},
},
select: {
allowDynamicBooking: true,
},
});
if (!users.length) {
return {
notFound: true,
};
}
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username: usernames.join("+"), eventSlug: slug });
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
booking,
user: usernames.join("+"),
slug,
away: false,
trpcState: ssr.dehydrate(),
isBrandingHidden: false,
themeBasis: null,
},
};
}
async function getUserPageProps(context: GetServerSidePropsContext) {
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const username = usernames[0];
const { rescheduleUid } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
},
select: {
away: true,
hideBranding: true,
},
});
if (!user) {
return {
notFound: true,
};
}
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug });
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
booking,
away: user?.away,
user: username,
slug,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
themeBasis: username,
},
};
}
const paramsSchema = z.object({
type: z.string().transform((s) => slugify(s)),
user: z.string().transform((s) => getUsernameList(s)),
});
// Booker page fetches a tiny bit of data server side, to determine early
// whether the page should show an away state or dynamic booking not allowed.
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { user } = paramsSchema.parse(context.params);
const isDynamicGroup = user.length > 1;
return isDynamicGroup ? await getDynamicGroupPageProps(context) : await getUserPageProps(context);
};
@@ -1,11 +0,0 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[type]";
export { default } from "../[type]";
// Somehow these types don't accept the {notFound: true} return type.
// Probably still need to fix this. I don't know why this isn't allowed yet.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
@@ -1,7 +0,0 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../../[user]";
export { default } from "../../[user]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
@@ -1,139 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { Booker } from "@calcom/atoms";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away, isBrandingHidden, isTeamEvent }: PageProps) {
return (
<main className="flex h-full min-h-[100dvh] items-center justify-center">
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
isTeamEvent={isTeamEvent}
/>
</main>
);
}
Type.PageWrapper = PageWrapper;
async function getUserPageProps(context: GetServerSidePropsContext) {
const { link, slug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const hashedLink = await prisma.hashedLink.findUnique({
where: {
link,
},
select: {
eventTypeId: true,
eventType: {
select: {
users: {
select: {
username: true,
},
},
team: {
select: {
id: true,
},
},
},
},
},
});
const username = hashedLink?.eventType.users[0]?.username;
if (!hashedLink || !username) {
return {
notFound: true,
};
}
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
},
select: {
away: true,
hideBranding: true,
},
});
if (!user) {
return {
notFound: true,
};
}
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
const isTeamEvent = !!hashedLink.eventType?.team?.id;
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug, isTeamEvent });
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
booking,
away: user?.away,
user: username,
slug,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
// Sending the team event from the server, because this template file
// is reused for both team and user events.
isTeamEvent,
},
};
}
const paramsSchema = z.object({ link: z.string(), slug: z.string().transform((s) => slugify(s)) });
// Booker page fetches a tiny bit of data server side, to determine early
// whether the page should show an away state or dynamic booking not allowed.
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
return await getUserPageProps(context);
};
@@ -1,104 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { Booker } from "@calcom/atoms";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { classNames } from "@calcom/lib";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
return (
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
isTeamEvent
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
isTeamEvent
/>
</main>
);
}
Type.PageWrapper = PageWrapper;
const paramsSchema = z.object({
type: z.string().transform((s) => slugify(s)),
slug: z.string().transform((s) => slugify(s)),
});
// Booker page fetches a tiny bit of data server side:
// 1. Check if team exists, to show 404
// 2. If rescheduling, get the booking details
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const team = await prisma.team.findFirst({
where: {
slug: teamSlug,
},
select: {
id: true,
hideBranding: true,
},
});
if (!team) {
return {
notFound: true,
};
}
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({
username: teamSlug,
eventSlug: meetingSlug,
isTeamEvent: true,
});
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
booking,
away: false,
user: teamSlug,
teamId: team.id,
slug: meetingSlug,
trpcState: ssr.dehydrate(),
isBrandingHidden: team?.hideBranding,
themeBasis: null,
},
};
};
@@ -1,9 +0,0 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[type]";
export { default } from "../[type]";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
@@ -1,7 +0,0 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../../../team/[slug]";
export { default } from "../../../team/[slug]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
@@ -4,10 +4,10 @@ import prisma from "@calcom/prisma";
import PageWrapper from "@components/PageWrapper";
import type { PageProps as UserTypePageProps } from "../../../new-booker/[user]/[type]";
import UserTypePage, { getServerSideProps as GSSUserTypePage } from "../../../new-booker/[user]/[type]";
import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../new-booker/team/[slug]/[type]";
import type { PageProps as TeamTypePageProps } from "../../../new-booker/team/[slug]/[type]";
import type { PageProps as UserTypePageProps } from "../../../[user]/[type]";
import UserTypePage, { getServerSideProps as GSSUserTypePage } from "../../../[user]/[type]";
import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../team/[slug]/[type]";
import type { PageProps as TeamTypePageProps } from "../../../team/[slug]/[type]";
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const team = await prisma.team.findFirst({
@@ -55,8 +55,9 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
type Props = UserTypePageProps | TeamTypePageProps;
export default function Page(props: Props) {
if ((props as TeamTypePageProps)?.teamId) return TeamTypePage(props as TeamTypePageProps);
return UserTypePage(props as UserTypePageProps);
if ((props as TeamTypePageProps)?.teamId) return <TeamTypePage {...(props as TeamTypePageProps)} />;
return <UserTypePage {...(props as UserTypePageProps)} />;
}
Page.PageWrapper = PageWrapper;
Page.isBookingPage = true;
@@ -6,7 +6,7 @@ import PageWrapper from "@components/PageWrapper";
import type { UserPageProps } from "../../../[user]";
import UserPage, { getServerSideProps as GSSUserPage } from "../../../[user]";
import type { TeamPageProps } from "../../../team/[slug]";
import type { PageProps as TeamPageProps } from "../../../team/[slug]";
import TeamPage, { getServerSideProps as GSSTeamPage } from "../../../team/[slug]";
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
@@ -33,8 +33,9 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
type Props = UserPageProps | TeamPageProps;
export default function Page(props: Props) {
if ((props as TeamPageProps)?.team) return TeamPage(props as TeamPageProps);
return UserPage(props as UserPageProps);
if ((props as TeamPageProps)?.team) return <TeamPage {...(props as TeamPageProps)} />;
return <UserPage {...(props as UserPageProps)} />;
}
Page.isBookingPage = true;
Page.PageWrapper = PageWrapper;
+8 -11
View File
@@ -12,14 +12,11 @@ export default function Type() {
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { uid: bookingId } = z
const { uid: bookingId, seatReferenceUid } = z
.object({ uid: z.string(), seatReferenceUid: z.string().optional() })
.parse(context.query);
let seatReferenceUid;
const uid = await maybeGetBookingUidFromSeat(prisma, bookingId);
if (uid) {
seatReferenceUid = bookingId;
}
const booking = await prisma.booking.findUnique({
where: {
uid,
@@ -39,6 +36,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
slug: true,
},
},
seatsPerTimeSlot: true,
},
},
dynamicEventSlugRef: true,
@@ -74,15 +72,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
"/" +
eventType?.slug;
const destinationUrl = new URLSearchParams();
if (seatReferenceUid) {
destinationUrl.set("rescheduleUid", seatReferenceUid);
} else {
destinationUrl.set("rescheduleUid", bookingId);
}
destinationUrl.set("rescheduleUid", seatReferenceUid || bookingId);
return {
redirect: {
destination: `/${eventPage}?${destinationUrl.toString()}`,
destination: `/${eventPage}?${destinationUrl.toString()}${
eventType.seatsPerTimeSlot ? "&bookingUid=null" : ""
}`,
permanent: false,
},
};
@@ -6,6 +6,8 @@ import { WizardLayout, Meta } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
export { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
const AboutOrganizationPage = () => {
const { t } = useLocale();
const router = useRouter();
@@ -7,6 +7,8 @@ import { WizardLayout, Meta } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
export { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
const AddNewTeamsPage = () => {
const { t } = useLocale();
const router = useRouter();
@@ -7,6 +7,8 @@ import { WizardLayout, Meta } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
export { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
const OnboardTeamMembersPage = () => {
const { t } = useLocale();
const router = useRouter();

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