Compare commits

...
103 Commits
Author SHA1 Message Date
zomars 5d775bd4a3 v2.3.6
release.yml / release (push) Canceled after 0s
2022-12-07 14:02:24 -07:00
Nafees Nazik 4e02b22472 fix: add optimistic update for disabling forms (#5547) 2022-12-07 13:55:41 -07:00
4686d3ff48 Improve multilingualism and fix search routes (#5334)
* fixed search routes

* made static text multilingual and fixed german translations

# Conflicts:
#	apps/web/components/availability/Schedule.tsx

* delete empty file created by fixing merge conflicts

* fixing TextField placeholder by passing attendeeName and fixed json variable

* Using useLocal to modify static text to make it multilingual, and passing the correct variables for brand and mail

* seperated whitelabel and improved translations

* added missing translation

* added missing translation for webhooks

* Updated AdminAppsView with dynamic translations.
Added translations for german and english files only.

* changed back to one liner

* updated german and english translations for impersonation.tsx

* updated german and english translations for impersonation.tsx and users.tsx. added missing german translation for timeformat_profile_hint

* updated german and english translations for team-billing-view.tsx

* updated german and english translations for LicenseRequired.tsx

* updated routes for profile and avatar

* yarn.lock updated from newer changes

* Revert " yarn.lock updated from newer changes"

This reverts commit efd9a90bf774371a331861e4fb441ab4f4d0b7fd.

* sanitize dangerouslySetInnerHTML to prevent xss attacks

* tried to fix window title flicker

* changed ssdInit to ssrInit for getServerSideProps. Serverside translation works but current route still set as a window title

* flicker with route in window title is caused here. It is not necessary to check if isPublic and session is false because it already gets checked in useRedirectToLoginIfUnauthenticated hook.

* fixed window title translation flicker for availability page

* fixed window title translation flicker for teams page

* fixed window title translation flicker for workflow page

* fixed error that div may not be rendered within p element

* fixed window title translation flicker for booking page

* fixed window title translation flicker by adding getServerSideProps

* Only set HeadSeo if an page title exists. If window title is set by the Meta component, shell is causing a flicker because it overwrites the title which is set by Meta. It is a problem especially for settings pages.

* fixed window title translation flicker by adding the Meta component to the skeleton

* fixed condition

* removed condition and added withoutSeo for settings pages

* using translations for create team page further fixed flicker for window title

* fixed flicker for window title for event-type creation page

* fixed flicker for window title for availability creation page

* fixed flicker for window title for sso page

* updated conferencing en translation

* added meta.CTA back it was mistakenly removed

* fixed flicker for workflows page

* fixed missing variable

* Update packages/ui/v2/core/Shell.tsx

* Delete index.tsx

* Update sso.tsx

* Updates subdmoules

Co-authored-by: maxi <maximilian.oehrlein@clicksports.de>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2022-12-07 13:53:44 -07:00
zomars 02e9d8d45f Sync submodules 2022-12-07 13:40:37 -07:00
Udit Takkar 7ce5096f13 fix: resetting input on cancel and refactoring input fiedl (#5522)
* fix: resetting input on cancel and refactoring input fiedl

* feat: add username field in /usersettings

* chore: remove unused

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

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-12-07 13:15:15 -07:00
Udit Takkar 83e51e9b9d fix: use trim in zod (#5915)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-12-07 13:12:53 -07:00
Udit TakkarandPeer Richelsen dca3278c40 fix: zoom video (#5910)
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: Peer Richelsen <peeroke@gmail.com>
2022-12-07 13:12:26 -07:00
Udit Takkar 7501635fbf fix: delete use when 2fa is disabled (#5899)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-12-07 13:12:14 -07:00
Peer Richelsen 80c96ea5ac remove "Add bugs to Bug Hunting" workflow (#5901)
not needed anymore
2022-12-07 13:09:01 -07:00
Hariom BalharaandPeer Richelsen 8c001e0d34 Fix reschedule embed (#5861)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-07 13:08:34 -07:00
Joe Au-Yeungandalannnc bf275497a2 MS Teams use joinWebUrl from response body (#5886)
* Add joinWebUrl to event

* Remove console.log

Co-authored-by: alannnc <alannnc@gmail.com>
2022-12-07 12:56:15 -07:00
6e351a4b20 fix/premium-username-flow (#5815)
* Removes user plan logic for stripe sub

* Update hardcoded price

* Removed checkoutSessionId metadata and fix for trpc query

* Remove or comment team billing unused code

* Add todo note

* Fix type checks

* Change function that was renamed

* Remove duplicated test

* remove plan when creating user on test

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-07 12:55:47 -07:00
github-actions[bot]andCrowdin Bot 5360cea3bf New Crowdin translations by Github Action (#5919)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-07 20:49:45 +01:00
+2 2f36985462 /availability - Add ability to set as default in the dropdown (#5717)
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: gGh00sTt <coolmagnas@gmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: gGh00sTt <coolmagnas@gmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2022-12-07 12:35:55 -07:00
6b26e9a7de Harsh/fix timeformat hint (#5918)
* fix: 404 page for undefined events

* remove prev code merge

* fix: timeformat profile hint

Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <root@pop-os.localdomain>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-07 18:22:43 +00:00
github-actions[bot]andCrowdin Bot d7655f0521 New Crowdin translations by Github Action (#5914)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-07 19:10:15 +01:00
sean-brydonandLeo Giovanetti f4ed345a87 Fixes disable branding for teams and users (#5913)
* Adding new nextauth object

* Use correct brand hidden values

* Move check to getprops instead of a function

* Update apps/web/components/booking/pages/AvailabilityPage.tsx

* Update apps/web/components/booking/pages/AvailabilityPage.tsx

* Update apps/web/pages/[user]/[type].tsx

Co-authored-by: Leo Giovanetti <hello@leog.me>

* Update apps/web/pages/api/auth/[...nextauth].tsx

Co-authored-by: Leo Giovanetti <hello@leog.me>

* Update apps/web/pages/api/auth/[...nextauth].tsx

Co-authored-by: Leo Giovanetti <hello@leog.me>

Co-authored-by: Leo Giovanetti <hello@leog.me>
2022-12-07 15:04:04 +00:00
SumedhandPeer Richelsen c3122c82e4 fix/cant-create-team-name-taken (#5911)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-07 14:50:54 +00:00
Udit Takkar cc9b49131e fix: add toast and fix delete action (#5912)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-12-07 15:37:20 +01:00
github-actions[bot]andCrowdin Bot 190db1a0f6 New Crowdin translations by Github Action (#5903)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-06 23:25:53 +01:00
Cory Dransfeldt bd48a72e99 [Feature] Adds support for Plausible analytics to app store (#5906) 2022-12-06 21:19:16 +00:00
Nafees Nazik a6836a1488 fix: use the new docker compose command if it exists (#5852) 2022-12-06 11:41:50 -07:00
2b67ea9635 Harsh/fix 404 page for undefined events (#5700)
* fix: 404 page for undefined events

* Update apps/web/pages/[user]/[type].tsx

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2022-12-06 15:04:34 +00:00
Udit Takkar a9117c2d36 fix: use tooltip portal (#5889)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-12-06 13:57:02 +00:00
+1 10d3b5f1b9 Remove the back arrow (#5402)
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: Matheus Benini <matheus_benini@hotmail.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: Matheus Benini <matheus_benini@hotmail.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
2022-12-06 13:02:58 +00:00
Peer Richelsen b757cb6ba1 added new teams empty screen (#5847)
* added new teams empty screen

* replaced svg background with smaller jpg

* nit

* nit

* nit

* nit

* show old emptyscreen for self-hosters

* nit
2022-12-05 15:20:33 -07:00
5968e0aa19 added EventReservationSchema to success page (#5777)
* added EventReservationSchema to success page

* removed duplicated of yarn.lock

Co-authored-by: René Müller <rene.mueller@clicksports.de>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 21:35:44 +00:00
zomars a06fffc4ec Adds missing translation 2022-12-05 14:24:09 -07:00
zomars 3312c84e7c Update turbo.json 2022-12-05 14:17:35 -07:00
Nafees NazikandPeer Richelsen d86710d5e9 fix: remove unnecessary title (#5853)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 21:11:10 +00:00
Udit Takkar 2d8e65c160 fix: ui of calendar item (#5879)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-12-05 20:39:52 +00:00
Udit TakkarandPeer Richelsen f74eaf7cb3 fix: increase edit location height (#5882)
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: Peer Richelsen <peeroke@gmail.com>
2022-12-05 20:18:45 +00:00
bcfd57c953 Bugfix/payment options (#5805)
* removed locale to use automatic detection in teams/lib/payments.ts

* removed payment_method_types to manage payment methods in the Dashboard

* added customer_update and automatic_tax to apply tax rates to payments

* added allow_promotion_codes to allow promotion codes

* removed multiple usages of `allow_promotion_codes` in payments.ts

Co-authored-by: René Müller <rene.mueller@clicksports.de>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-12-05 13:02:59 -07:00
c38588b93d New Crowdin translations by Github Action (#5842)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 20:59:12 +01:00
alannnc 32ed43574d Fix input parse to number (#5884) 2022-12-05 20:57:33 +01:00
38a5a22b9e Harsh/fix bookings name (#5848)
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <root@pop-os.localdomain>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 17:41:40 +00:00
Sascha FoersterandPeer Richelsen 2120e301c3 Update german translations (#5854)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 17:29:19 +00:00
Bailey Pumfleet dd0aec4e97 Fix Kbar translation (#5867) 2022-12-05 14:43:31 +00:00
Peer Richelsen 3815b86258 added norwegian to i18n (#5866) 2022-12-05 13:58:00 +00:00
Hariom BalharaandPeer Richelsen 7a6545bd03 Wordpress App Files cleanup (#5858)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 13:42:38 +00:00
Udit TakkarandPeer Richelsen 11f467af5d fix: toggle (#5865)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 13:28:21 +00:00
0930cf50b6 BugFix: Link based Apps installation - Opens 2 tabs instead of 1 (#5859)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-05 14:13:01 +01:00
c37cddd0b7 feat: add each app from the app store to KBar (#5724)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-05 12:13:31 +00:00
09253f83bf [CAL-463] Clicking Members from event types screen should take you to the members page (#5857)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: Matheus Benini <matheus_benini@hotmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-12-05 12:12:37 +00:00
Leo GiovanettiandBailey Pumfleet 1cdcebac01 Requires Confirmation Threshold (#5825)
* WIP + react errors fixed

* Booking and Availability page treatment

* Update BookingPage.tsx

Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-12-05 12:12:14 +00:00
sean-brydon 58270fa653 Team impersonation (#5863)
* Re Implement frontend logic to impersonate team members

* Refactor dialog + correct disable toggle.

* fix translation

* Update packages/features/ee/impersonation/lib/ImpersonationProvider.ts
2022-12-05 12:03:36 +00:00
Peer Richelsen 1a26a254ab added team subdomain to whereby validation (#5850) 2022-12-03 19:32:40 +00:00
Udit Takkarandalannnc a36d15894d fix: user infer props and remove avatarSSr from onboarding (#5549)
Co-authored-by: alannnc <alannnc@gmail.com>
2022-12-03 19:20:18 +00:00
Sumedh cf077fd33c fix/sidebar-profiledropdown-wired-hoverstate (#5832) 2022-12-03 15:12:12 +00:00
sean-brydonandPeer Richelsen 0c979d3604 Fix imports + buttons (#5831)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-03 14:53:07 +00:00
zomars 33bc786bb3 API scripts 2022-12-02 15:25:13 -07:00
zomars a7f16462db v2.3.5
release.yml / release (push) Canceled after 0s
2022-12-02 13:33:41 -07:00
zomars 2c7fcbcf51 hotfix: 404 for mixed case usernames 2022-12-02 13:32:56 -07:00
Syed Ali ShahbazandAlex van Andel d3e4d26fa5 fixes UTC consideration in CalDAV (#5827)
* fixes UTC consideration

* typecheck fix

* code quality

* comment location

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-02 19:46:38 +00:00
Carina WollendorferandCarinaWolli 3be6c05722 Fixes some translation issues (#5841)
* fix french translation for rescheduled email

* fix translations for team invite

* fix rescheduling email in german

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-12-02 17:56:48 +00:00
Carina WollendorferandCarinaWolli e293e590bd remove if and at to query (#5838)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-12-02 13:43:07 +00:00
+1 d8c1c103a1 [CAL-103] All Bookings show up under Today and Today should be uppercase (#5155)
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-02 10:08:12 +00:00
Joe Au-YeungandAlan 19d00e691c Fix attendees on a booking with seats and payments, navigate to the payment page. (#5788)
* Payment booking logic to seats

* Pass email to success page from payment page

* Fix type error

Co-authored-by: Alan <alannnc@gmail.com>
2022-12-02 00:12:06 +00:00
78aa4aecef Settings: Tab item line height fix && Stop x-axis overflow of settings tab (#5699)
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-01 23:09:14 +00:00
3ab002e547 5444 cal 339 radio option in additional questions on public booking page (#5804)
* Use field array intro

* WIP - form submitting wrong form

* WIP with fake useFormHook

* WORKING! OMG

Co-authored-by: Alex <alex@cal.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>

* Booking Page styling

* Fix duplicate fields

* Radio string

* Type error

* Linting errors

* Remove unused duplicate file

* Fixed user related type error

* remove log

* Remove console logs

* remove console log

* fix dark mode text and comment style

Co-authored-by: Alex <alex@cal.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-12-01 21:53:52 +00:00
Nafees Nazikandalannnc 7f461bc275 fix: padding (#5808)
Co-authored-by: alannnc <alannnc@gmail.com>
2022-12-01 13:30:57 -07:00
Carina WollendorferandCarinaWolli 571176102e add smsReminderNumber to webhook payload (#5823)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-12-01 12:51:59 -07:00
github-actions[bot]andCrowdin Bot 8f1d3fbc50 New Crowdin translations by Github Action (#5816)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-01 15:42:24 +00:00
Joe Au-YeungandAlex van Andel 5e88ad50ea If no location is set, default to Cal Video (#5812)
* Default to Cal Video

* Show web conferencing details to follow on success page

* Show web conferencing details to follow on success page

* Show web conferencing details to follow on success page

* Type fixes

* Pass tests

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-01 10:20:01 -05:00
Alex van Andel 056406bdd9 v2.3.4
release.yml / release (push) Canceled after 0s
2022-12-01 11:18:18 +00:00
Alex van AndelandOmar López 58af5a18a4 Moved some files around & made some fixes (#5811)
* Moved some files around & made some fixes

* Update apps/web/pages/event-types/index.tsx

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

* Update apps/web/pages/apps/installed/[category].tsx

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

* Update apps/web/components/apps/App.tsx

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

* Update apps/web/components/apps/CalendarListContainer.tsx

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

* Update apps/web/pages/settings/my-account/calendars.tsx

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

Co-authored-by: Omar López <zomars@me.com>
2022-12-01 00:59:27 +00:00
zomars 90b6581d76 Allows promo codes for team subscriptions 2022-11-30 17:36:04 -07:00
sean-brydon b33dcc8841 Weird (#5818) 2022-11-30 17:11:59 -07:00
d951a5b872 Allows brand customization (#5329)
* adjustments for each language json file:
- changed every Cal or Cal.com with a variable to make it possible to change that with a custom brand
- fix and renamed  ATTENDEE with attendeeName

* added two new variables for appName and support mail address. so everybody can change it via env

* changed static Cal or Cal.com with new defined constants

* Using useLocal to modify static text to make it multilingual, and passing the correct variables for brand and mail

* adding new readable variables for brand, website domain and mail address

* fixed search routes

* made static text multilingual and fixed german translations

* Revert "fixed search routes"
moved changes in another pr
This reverts commit e6ba11a1ec7821d8c16c502d0357f6d5fcdb1958.

* revert non whitelabel changes and moved it into another pr

* revert attendeeName fix

* reverted translation fixes and moved them in another pr

* changed back to "Cal.com Logo"

* changed back to "https://console.cal.com"

* added new env variable for company name and replaced some domainName variables in language files

* changed default for COMPANY_NAME to Cal.com, Inc.

* changed Cal.com to APP_NAME for mail templates

* Dropped website domain in favor of app name

* Update .env.example

* Apply suggestions from code review

* Code review feedback

* Delete App.tsx

* Update packages/ui/Kbar.tsx

* added meta.CTA back it was mistakenly removed

* updated add members test

Co-authored-by: maxi <maximilian.oehrlein@clicksports.de>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2022-11-30 14:52:56 -07:00
Nafees NazikandPeer Richelsen 7122a9cfbf fix: padding (#5809)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-30 21:41:29 +00:00
f1b92c49fc App Store Search (#5704)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2022-11-30 20:51:44 +00:00
Leo Giovanettiandkodiakhq[bot] ef3ad36c2c Fixed listing formatting and save for custom inputs (#5795)
* Fixed formatting and save

* Additional tweaks

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-30 11:53:28 -07:00
072ce452b7 Catch the error that's thrown when no api_key is found (no daily) (#5696)
* fix/event-type-location-select (#5692)

* Fixed dialog components usage

* Fix styles

* Catch the error that's thrown when no api_key is found (no daily install)

Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-30 16:44:20 +00:00
Leo Giovanetti 0e0940f5f0 Fixing button/select styles (#5810) 2022-11-30 16:13:20 +00:00
Jeroen Reumkens 310936125d Changed select export to use v2 instead of v1. (#5715) 2022-11-30 08:42:45 +00:00
Peer Richelsen a9df3309b9 added wordpress plugin (#5168) 2022-11-29 21:57:26 +00:00
148d658f14 fix: update new team cancel button url (#5520)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-29 21:38:54 +00:00
Nafees NazikandPeer Richelsen 242d571d7d fix: missing rounded corners in team member settings (#5789)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-29 21:26:01 +00:00
dff49ec28a Rename /success to /manage and fix link in calendar event (#5719)
* fix need to make changes link in calendar event

* change /success link to /manage

* delete success.tsx file and use next.js rewrites

* fix e2e tests

* remove not needed waitForNavigation

* fix e2e tests

* rename manage?uid to booking/uid

* fix rewrite

* remove not needed export

* fix rescheduling e2e tests

* Minor fixes/cleannup

* Update BookingPage.tsx

* Moves cancel page to rewrite

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2022-11-29 20:27:29 +00:00
Leo Giovanetti 0c746ec10f Fixing add button (#5780) 2022-11-29 19:18:11 +00:00
Leo Giovanetti 901f54ca3a Fixing create event (#5781) 2022-11-29 11:47:08 -07:00
alannnc 16fda5f599 Enables require confirmation after payment its made (#5671) 2022-11-29 18:28:02 +00:00
d3e9adf382 allow cancelling and rescheduling for not confirmed bookings (#5770)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-11-29 16:03:39 +00:00
cf559e0726 New Crowdin translations by Github Action (#5753)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-11-29 15:51:24 +00:00
Udit Takkar 8447b1bbd5 fix: use shouldDirty flag (#5775)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-11-29 15:49:02 +00:00
Alex van Andel 0d03006653 v2.3.3
release.yml / release (push) Canceled after 0s
2022-11-29 09:40:40 +00:00
Alex van Andel d44c5fd8b0 Fixes availability to reset to default on save non-team (#5710) 2022-11-29 09:36:05 +00:00
checkly[bot] 74aa366f1e [Checkly] adds "Location Link" code (#5752)
Co-authored-by: checkly[bot] <54914329+checkly[bot]@users.noreply.github.com>
2022-11-28 16:52:24 -07:00
sean-brydon 7ebea57602 Dialog Api improvements (#5714) 2022-11-28 19:14:38 +00:00
26d675e162 Allow clients to choose different duration for a meeting (#5660)
* WIP

* Missing translation

* Default duration clears correctly

* Fixing hydration error

* Applying feedback

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-28 18:14:01 +00:00
Jeroen Reumkensandkodiakhq[bot] e16d29e916 Added optional chaining to embed iframe using window object, where objects were potentially undefined. (#5727)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-28 09:54:14 -07:00
zomars 0e89fb9f6a Update release.yml 2022-11-28 09:07:30 -07:00
zomars 43fb410a28 v2.3.2
release.yml / release (push) Canceled after 0s
2022-11-28 09:02:55 -07:00
b8bbfe8a3f fix: ui of reschedule dialog (#5691)
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: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-28 08:52:42 -07:00
Leo GiovanettiandAlex van Andel 1f74fc394a Fixing calWarnings error on reschedule (#5711)
* Fixing calWarnings error on reschedule

* Update packages/core/CalendarManager.ts

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

* Update packages/core/CalendarManager.ts

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

* Going back on potential fix

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-11-28 15:43:24 +00:00
Carina WollendorferandCarinaWolli 9a12b5a0ac fix e2e tests (#5726)
* fix e2e tests

* remove not needed waitForNavigation

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-11-28 15:27:07 +00:00
Jeroen Reumkens 2ce2c1074b Don't return admin_min error when we're not validating for admin password. (#5713) 2022-11-28 07:57:02 -07:00
Nafees NazikandPeer Richelsen 0ef80da260 fix: remove unnecessary buttons from the image uploader modal (#5725)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-28 14:19:34 +00:00
github-actions[bot]andCrowdin Bot 1e50488cfe New Crowdin translations by Github Action (#5722)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-11-28 12:13:33 +00:00
sean-brydonandPeer Richelsen ae9f66c9ef InputField with suffix select (#5683)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-28 11:20:49 +00:00
798a4b0090 New Crowdin translations by Github Action (#5705)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-27 11:50:40 +00:00
Harsh SinghandGuest dcd9ff72b7 fix banner z-index (#5707)
* fix: 404 page for undefined events

* remove prev code merge

* change: z-index value of banner

Co-authored-by: Guest <guest@pop-os.localdomain>
2022-11-27 11:49:16 +00:00
GitStart 0b5a0186bc [CAL-280] mobile booking: selecting a day does not scroll first time (#5682) 2022-11-26 23:10:31 +00:00
283 changed files with 7564 additions and 4810 deletions

No files matched your search

+5
View File
@@ -140,3 +140,8 @@ SENDGRID_SYNC_API_KEY=
# Sentry # Sentry
NEXT_PUBLIC_SENTRY_DSN= NEXT_PUBLIC_SENTRY_DSN=
SENTRY_IGNORE_API_RESOLUTION_ERROR= SENTRY_IGNORE_API_RESOLUTION_ERROR=
# Change your Brand
NEXT_PUBLIC_APP_NAME="Cal.com"
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
-22
View File
@@ -1,22 +0,0 @@
name: Add bugs to Bug Hunting
on:
issues:
types:
- opened
- labeled
pull_request:
types:
- opened
- labeled
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.1.0
with:
project-url: https://github.com/orgs/calcom/projects/9
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
labeled: 🙋🏻‍♂️help wanted
+1
View File
@@ -11,6 +11,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
ref: main # Always checkout main even for tagged releases
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GH_ACCESS_TOKEN }} token: ${{ secrets.GH_ACCESS_TOKEN }}
- run: git push origin +main:production - run: git push origin +main:production
+35
View File
@@ -0,0 +1,35 @@
/**
* This is a Playwright script Checkly generated for you based on your Vercel project.
* To learn more about Browser checks and Playwright visit: https://www.checklyhq.com/docs/browser-checks
*/
// Create a Chromium browser
const { chromium } = require("playwright");
// Checkly supports top level await, but we wrap your code in an async function so you can run it locally too.
async function run() {
const browser = await chromium.launch();
const page = await browser.newPage();
const targetUrl = process.env.ENVIRONMENT_URL || "https://cal.com";
await page.goto(`${targetUrl}/rick/test-location-link`);
await page.waitForSelector('[data-testid="day"][data-disabled="false"]');
await page.click('[data-testid="day"][data-disabled="false"]');
await page.waitForSelector('[data-testid="time"]');
await page.click('[data-testid="time"]');
await page.waitForSelector("#name");
await page.click("#name");
await page.type("#name", "Calcom");
await page.type('[name="email"]', "cal@cal.com");
await page.waitForSelector('[data-testid="confirm-book-button"]');
await page.click('[data-testid="confirm-book-button"]');
await browser.close();
}
run();
Submodule apps/api updated: 055699f612...41d22c8ccb.
+9 -10
View File
@@ -4,6 +4,7 @@ import { NextRouter, useRouter } from "next/router";
import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react"; import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react";
import { components, ControlProps } from "react-select"; import { components, ControlProps } from "react-select";
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { import {
Button, Button,
@@ -20,8 +21,6 @@ import {
TextField, TextField,
} from "@calcom/ui"; } from "@calcom/ui";
import { EMBED_LIB_URL, WEBAPP_URL } from "@lib/config/constants";
import ColorPicker from "@components/ui/colorpicker"; import ColorPicker from "@components/ui/colorpicker";
import Select from "@components/ui/form/Select"; import Select from "@components/ui/form/Select";
@@ -498,7 +497,9 @@ const tabs = [
return ( return (
<> <>
<div> <div>
<small className="flex py-4 text-neutral-500">{t("place_where_cal_widget_appear")}</small> <small className="flex py-4 text-neutral-500">
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
</small>
</div> </div>
<TextArea <TextArea
data-testid="embed-code" data-testid="embed-code"
@@ -619,13 +620,13 @@ const ChooseEmbedTypesDialogContent = () => {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
return ( return (
<DialogContent type="creation" useOwnActionButtons size="lg"> <DialogContent type="creation" size="lg">
<div className="mb-4"> <div className="mb-4">
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title"> <h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
{t("how_you_want_add_cal_site")} {t("how_you_want_add_cal_site", { appName: APP_NAME })}
</h3> </h3>
<div> <div>
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site")}</p> <p className="text-sm text-gray-500">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
</div> </div>
</div> </div>
<div className="flex items-start"> <div className="flex items-start">
@@ -797,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
]; ];
return ( return (
<DialogContent size="xl" className="p-0.5" type="creation" useOwnActionButtons> <DialogContent size="xl" className="p-0.5" type="creation">
<div className="flex"> <div className="flex">
<div className="flex w-1/3 flex-col bg-gray-50 p-8"> <div className="flex w-1/3 flex-col bg-gray-50 p-8">
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title"> <h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
@@ -1095,9 +1096,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
{t("copy_code")} {t("copy_code")}
</Button> </Button>
) : null} ) : null}
<DialogClose asChild> <DialogClose />
<Button color="secondary">{t("Close")}</Button>
</DialogClose>
</div> </div>
</div> </div>
); );
+2 -6
View File
@@ -156,12 +156,8 @@ export default function ImageUploader({
</div> </div>
</div> </div>
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse"> <div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
<DialogClose asChild> <DialogClose onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</DialogClose>
<Button onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</Button> <DialogClose color="secondary">{t("cancel")}</DialogClose>
</DialogClose>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -61,12 +61,10 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
<Select <Select
name="additionalCalendar" name="additionalCalendar"
placeholder={ placeholder={
<div className="flex justify-start text-black"> <Button StartIcon={Icon.FiPlus} color="secondary">
<Icon.FiPlus className="-ml-1 h-4 w-4 ltr:mr-2 rtl:ml-2 rtl:-mr-1" /> {t("add")}
<p>{t("add")}</p> </Button>
</div>
} }
className="min-w-44"
options={options} options={options}
isSearchable={false} isSearchable={false}
isLoading={isLoading} isLoading={isLoading}
@@ -80,11 +78,16 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
width: "max-content", width: "max-content",
right: "0", right: "0",
}), }),
indicatorSeparator: (defaultStyles) => ({
...defaultStyles,
display: "none",
}),
control: (defaultStyles) => ({ control: (defaultStyles) => ({
...defaultStyles, ...defaultStyles,
padding: "0", padding: "0",
border: "0", border: "0",
borderRadius: "inherit", borderRadius: "6px",
minHeight: "auto",
}), }),
dropdownIndicator: (defaultStyles) => ({ dropdownIndicator: (defaultStyles) => ({
...defaultStyles, ...defaultStyles,
+9 -13
View File
@@ -4,20 +4,14 @@ import React, { useState } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation"; import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton } from "@calcom/app-store/components"; import { InstallAppButton } from "@calcom/app-store/components";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired"; import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { App as AppType } from "@calcom/types/App"; import { App as AppType } from "@calcom/types/App";
import { import { Button, Icon, Shell, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
Button,
DisconnectIntegration,
Icon,
Shell,
showToast,
SkeletonButton,
SkeletonText,
} from "@calcom/ui";
import HeadSeo from "@components/seo/head-seo"; import HeadSeo from "@components/seo/head-seo";
@@ -48,7 +42,7 @@ const Component = ({
const mutation = useAddAppMutation(null, { const mutation = useAddAppMutation(null, {
onSuccess: (data) => { onSuccess: (data) => {
if (data.setupPending) return; if (data?.setupPending) return;
showToast(t("app_successfully_installed"), "success"); showToast(t("app_successfully_installed"), "success");
}, },
onError: (error) => { onError: (error) => {
@@ -201,7 +195,7 @@ const Component = ({
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4> <h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
<span> <span>
{price === 0 ? ( {price === 0 ? (
"Free" t("free_to_use_apps")
) : ( ) : (
<> <>
{Intl.NumberFormat("en-US", { {Intl.NumberFormat("en-US", {
@@ -279,8 +273,10 @@ const Component = ({
)} )}
</ul> </ul>
<hr className="my-8" /> <hr className="my-8" />
<span className="leading-1 block text-xs text-gray-500">{t("every_app_published")}</span> <span className="leading-1 block text-xs text-gray-500">
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com"> {t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
<Icon.FiFlag className="inline h-3 w-3" /> {t("report_app")} <Icon.FiFlag className="inline h-3 w-3" /> {t("report_app")}
</a> </a>
</div> </div>
@@ -3,13 +3,13 @@ import Link from "next/link";
import { Fragment } from "react"; import { Fragment } from "react";
import { InstallAppButton } from "@calcom/app-store/components"; import { InstallAppButton } from "@calcom/app-store/components";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector"; import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { import {
Alert, Alert,
Button, Button,
DisconnectIntegration,
EmptyScreen, EmptyScreen,
Icon, Icon,
List, List,
@@ -1,9 +1,12 @@
import { SchedulingType } from "@prisma/client"; import { SchedulingType } from "@prisma/client";
import { FC, ReactNode } from "react"; import { FC, ReactNode, useEffect } from "react";
import dayjs from "@calcom/dayjs";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui"; import { Icon, Badge } from "@calcom/ui";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { UserAvatars } from "@components/booking/UserAvatars"; import { UserAvatars } from "@components/booking/UserAvatars";
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML"; import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
@@ -39,7 +42,35 @@ interface Props {
const BookingDescription: FC<Props> = (props) => { const BookingDescription: FC<Props> = (props) => {
const { profile, eventType, isBookingPage = false, children } = props; const { profile, eventType, isBookingPage = false, children } = props;
const { date: bookingDate } = useRouterQuery("date");
const { t } = useLocale(); const { t } = useLocale();
const { duration, setQuery: setDuration } = useRouterQuery("duration");
useEffect(() => {
if (eventType.metadata?.multipleDuration !== undefined) {
if (!duration) {
setDuration(eventType.length);
} else {
if (!eventType.metadata?.multipleDuration.includes(Number(duration))) {
setDuration(eventType.length);
}
}
}
}, []);
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 ( return (
<> <>
<UserAvatars <UserAvatars
@@ -75,29 +106,61 @@ const BookingDescription: FC<Props> = (props) => {
</div> </div>
</div> </div>
)} )}
{eventType?.requiresConfirmation && ( {requiresConfirmation && (
<div <div
className={classNames( className={classNames(
"flex items-center", "items-top flex",
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600" isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
)}> )}>
<div> <div>
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " /> <Icon.FiCheckSquare className="mr-[10px] ml-[2px] inline-block h-4 w-4 " />
</div> </div>
{t("requires_confirmation")} {requiresConfirmationText}
</div> </div>
)} )}
<AvailableEventLocations <AvailableEventLocations
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]} locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
/> />
<p <div
className={classNames( className={classNames(
"text-sm font-medium", "flex flex-nowrap text-sm font-medium",
isBookingPage && "dark:text-darkgray-600 text-gray-600" isBookingPage && "dark:text-darkgray-600 text-gray-600"
)}> )}>
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" /> <Icon.FiClock
{eventType.length} {t("minutes")} className={classNames(
</p> "min-h-4 min-w-4 mr-[10px] ml-[2px] inline-block",
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"
size="lg"
className={classNames(
duration === dur.toString()
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
: "hover:bg-darkgray-200 dark:hover:bg-darkmodebrand hover:text-darkgray-900 dark:hover:text-darkmodebrandcontrast dark:bg-darkgray-200 bg-gray-200 text-gray-900 dark:text-white",
"cursor-pointer"
)}
onClick={() => {
setDuration(dur);
}}>
{dur} {t("minute_timeUnit")}
</Badge>
</li>
))}
</ul>
) : (
`${duration} ${t("minutes")}`
)
) : (
`${eventType.length} ${t("minutes")}`
)}
</div>
{children} {children}
</div> </div>
</> </>
@@ -118,7 +118,7 @@ function BookingListItem(booking: BookingItemProps) {
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"), label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
/* When cancelling we need to let the UI and the API know if the intention is to /* When cancelling we need to let the UI and the API know if the intention is to
cancel all remaining bookings or just that booking instance. */ cancel all remaining bookings or just that booking instance. */
href: `/success?uid=${booking.uid}&cancel=true${ href: `/booking/${booking.uid}?cancel=true${
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : "" isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}`, }`,
icon: Icon.FiX, icon: Icon.FiX,
@@ -195,11 +195,9 @@ function BookingListItem(booking: BookingItemProps) {
const onClickTableData = () => { const onClickTableData = () => {
router.push({ router.push({
pathname: "/success", pathname: `/booking/${booking.uid}`,
query: { query: {
uid: booking.uid,
allRemainingBookings: isTabRecurring, allRemainingBookings: isTabRecurring,
listingStatus: booking.listingStatus,
email: booking.attendees[0] ? booking.attendees[0].email : undefined, email: booking.attendees[0] ? booking.attendees[0].email : undefined,
}, },
}); });
@@ -237,9 +235,7 @@ function BookingListItem(booking: BookingItemProps) {
/> />
<DialogFooter> <DialogFooter>
<DialogClose> <DialogClose />
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
<Button <Button
disabled={mutation.isLoading} disabled={mutation.isLoading}
@@ -284,6 +280,11 @@ function BookingListItem(booking: BookingItemProps) {
{t("pending_payment")} {t("pending_payment")}
</Badge> </Badge>
)} )}
{booking.paid && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
{t("paid")}
</Badge>
)}
{recurringDates !== undefined && ( {recurringDates !== undefined && (
<div className="mt-2 text-sm text-gray-400"> <div className="mt-2 text-sm text-gray-400">
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} /> <RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
@@ -1,7 +1,6 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { EventType } from "@prisma/client"; import { EventType } from "@prisma/client";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import { TFunction } from "next-i18next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useReducer, useEffect, useMemo, useState } from "react"; import { useReducer, useEffect, useMemo, useState } from "react";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
@@ -19,7 +18,6 @@ import {
} from "@calcom/embed-core/embed-iframe"; } from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding"; import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { WEBSITE_URL } from "@calcom/lib/constants";
import getStripeAppData from "@calcom/lib/getStripeAppData"; import getStripeAppData from "@calcom/lib/getStripeAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
@@ -33,6 +31,7 @@ import { Icon, DatePicker } from "@calcom/ui";
import { timeZone as localStorageTimeZone } from "@lib/clock"; import { timeZone as localStorageTimeZone } from "@lib/clock";
// import { timeZone } from "@lib/clock"; // import { timeZone } from "@lib/clock";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally"; import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { isBrandingHidden } from "@lib/isBrandingHidden"; import { isBrandingHidden } from "@lib/isBrandingHidden";
import Gates, { Gate, GateState } from "@components/Gates"; import Gates, { Gate, GateState } from "@components/Gates";
@@ -46,23 +45,6 @@ import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]"; import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]"; import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
// Get router variables
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
const router = useRouter();
const path = router.asPath.split("/");
path.pop(); // Remove the last item (where we currently are)
path.shift(); // Removes first item e.g. if we were visitng "/teams/test/30mins" the array will new look like ["teams","test"]
const slug = path.join("/");
return (
<div className="flex h-full flex-col justify-end">
<button title={t("profile")} onClick={() => router.replace(`${WEBSITE_URL}/${slug}`)}>
<Icon.FiArrowLeft className="dark:text-darkgray-600 h-4 w-4 text-black transition-opacity hover:cursor-pointer" />
<p className="sr-only">Go Back</p>
</button>
</div>
);
};
const useSlots = ({ const useSlots = ({
eventTypeId, eventTypeId,
eventTypeSlug, eventTypeSlug,
@@ -70,6 +52,7 @@ const useSlots = ({
endTime, endTime,
usernameList, usernameList,
timeZone, timeZone,
duration,
}: { }: {
eventTypeId: number; eventTypeId: number;
eventTypeSlug: string; eventTypeSlug: string;
@@ -77,6 +60,7 @@ const useSlots = ({
endTime?: Dayjs; endTime?: Dayjs;
usernameList: string[]; usernameList: string[];
timeZone?: string; timeZone?: string;
duration?: string;
}) => { }) => {
const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery( const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery(
{ {
@@ -86,6 +70,7 @@ const useSlots = ({
startTime: startTime?.toISOString() || "", startTime: startTime?.toISOString() || "",
endTime: endTime?.toISOString() || "", endTime: endTime?.toISOString() || "",
timeZone, timeZone,
duration,
}, },
{ {
enabled: !!startTime && !!endTime, enabled: !!startTime && !!endTime,
@@ -126,6 +111,7 @@ const SlotPicker = ({
}) => { }) => {
const [selectedDate, setSelectedDate] = useState<Dayjs>(); const [selectedDate, setSelectedDate] = useState<Dayjs>();
const [browsingDate, setBrowsingDate] = useState<Dayjs>(); const [browsingDate, setBrowsingDate] = useState<Dayjs>();
const { duration } = useRouterQuery("duration");
const { date, setQuery: setDate } = useRouterQuery("date"); const { date, setQuery: setDate } = useRouterQuery("date");
const { month, setQuery: setMonth } = useRouterQuery("month"); const { month, setQuery: setMonth } = useRouterQuery("month");
const router = useRouter(); const router = useRouter();
@@ -151,7 +137,7 @@ const SlotPicker = ({
setSelectedDate(dayjs.tz(date, timeZone)); setSelectedDate(dayjs.tz(date, timeZone));
} }
} }
}, [router.isReady, month, date, timeZone]); }, [router.isReady, month, date, duration, timeZone]);
const { i18n, isLocaleReady } = useLocale(); const { i18n, isLocaleReady } = useLocale();
const { slots: _1 } = useSlots({ const { slots: _1 } = useSlots({
@@ -161,6 +147,7 @@ const SlotPicker = ({
startTime: selectedDate?.startOf("day"), startTime: selectedDate?.startOf("day"),
endTime: selectedDate?.endOf("day"), endTime: selectedDate?.endOf("day"),
timeZone, timeZone,
duration,
}); });
const { slots: _2, isLoading } = useSlots({ const { slots: _2, isLoading } = useSlots({
eventTypeId: eventType.id, eventTypeId: eventType.id,
@@ -169,6 +156,7 @@ const SlotPicker = ({
startTime: browsingDate?.startOf("month"), startTime: browsingDate?.startOf("month"),
endTime: browsingDate?.endOf("month"), endTime: browsingDate?.endOf("month"),
timeZone, timeZone,
duration,
}); });
const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]); const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]);
@@ -229,14 +217,14 @@ function TimezoneDropdown({
return ( return (
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}> <Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 inline-block rounded-md px-2 py-2 text-left text-gray-600"> <Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
<p className="text-sm font-medium"> <p className="flex items-center text-sm font-medium">
<Icon.FiGlobe className="mr-[10px] ml-[2px] -mt-[2px] inline-block h-4 w-4" /> <Icon.FiGlobe className="min-h-4 min-w-4 mr-[10px] ml-[2px] -mt-[2px] inline-block" />
{timeZone} {timeZone}
{isTimeOptionsOpen ? ( {isTimeOptionsOpen ? (
<Icon.FiChevronUp className="ml-1 inline-block h-4 w-4" /> <Icon.FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
) : ( ) : (
<Icon.FiChevronDown className="ml-1 inline-block h-4 w-4" /> <Icon.FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
)} )}
</p> </p>
</Popover.Trigger> </Popover.Trigger>
@@ -258,23 +246,6 @@ const dateQuerySchema = z.object({
timeZone: z.string().optional().default(""), timeZone: z.string().optional().default(""),
}); });
const useRouterQuery = <T extends string>(name: T) => {
const router = useRouter();
const existingQueryParams = router.asPath.split("?")[1];
const urlParams = new URLSearchParams(existingQueryParams);
const query = Object.fromEntries(urlParams);
const setQuery = (newValue: string | number | null | undefined) => {
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
};
return { [name]: query[name], setQuery } as {
[K in T]: string | undefined;
} & { setQuery: typeof setQuery };
};
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps; export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => { const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
@@ -432,12 +403,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
{timezoneDropdown} {timezoneDropdown}
</BookingDescription> </BookingDescription>
{!isEmbed && (
<div className="mt-auto hidden md:block">
<GoBackToPreviousPage t={t} />
</div>
)}
{/* Temporarily disabled - booking?.startTime && rescheduleUid && ( {/* Temporarily disabled - booking?.startTime && rescheduleUid && (
<div> <div>
<p <p
@@ -477,7 +442,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
/> />
</div> </div>
</div> </div>
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && <PoweredByCal />} {(!restProps.isBrandingHidden || isEmbed) && <PoweredByCal />}
</div> </div>
</main> </main>
</div> </div>
@@ -31,6 +31,7 @@ import {
} from "@calcom/embed-core/embed-iframe"; } from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding"; import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { APP_NAME } from "@calcom/lib/constants";
import getStripeAppData from "@calcom/lib/getStripeAppData"; import getStripeAppData from "@calcom/lib/getStripeAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
@@ -38,11 +39,12 @@ import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { AddressInput, Button, EmailInput, Form, Icon, PhoneInput, Tooltip } from "@calcom/ui"; import { AddressInput, Button, EmailInput, Form, Icon, PhoneInput, Tooltip } from "@calcom/ui";
import { Group, RadioField } from "@calcom/ui";
import { asStringOrNull } from "@lib/asStringOrNull"; import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock"; import { timeZone } from "@lib/clock";
import { ensureArray } from "@lib/ensureArray"; import { ensureArray } from "@lib/ensureArray";
import useMeQuery from "@lib/hooks/useMeQuery"; import useRouterQuery from "@lib/hooks/useRouterQuery";
import createBooking from "@lib/mutations/bookings/create-booking"; import createBooking from "@lib/mutations/bookings/create-booking";
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking"; import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
import { parseDate, parseRecurringDates } from "@lib/parseDate"; import { parseDate, parseRecurringDates } from "@lib/parseDate";
@@ -85,9 +87,7 @@ const BookingPage = ({
...restProps ...restProps
}: BookingPageProps) => { }: BookingPageProps) => {
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
// Get user so we can determine 12/24 hour format preferences const { duration: queryDuration } = useRouterQuery("duration");
const query = useMeQuery();
const user = query.data;
const isEmbed = useIsEmbed(restProps.isEmbed); const isEmbed = useIsEmbed(restProps.isEmbed);
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left"; const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed; const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
@@ -103,6 +103,11 @@ const BookingPage = ({
{} {}
); );
const stripeAppData = getStripeAppData(eventType); const stripeAppData = getStripeAppData(eventType);
// Define duration now that we support multiple duration eventTypes
let duration = eventType.length;
if (queryDuration && !isNaN(Number(queryDuration))) {
duration = Number(queryDuration);
}
useEffect(() => { useEffect(() => {
if (top !== window) { if (top !== window) {
@@ -131,9 +136,8 @@ const BookingPage = ({
} }
return router.push({ return router.push({
pathname: "/success", pathname: `/booking/${uid}`,
query: { query: {
uid,
isSuccessBookingPage: true, isSuccessBookingPage: true,
email: bookingForm.getValues("email"), email: bookingForm.getValues("email"),
eventTypeSlug: eventType.slug, eventTypeSlug: eventType.slug,
@@ -147,9 +151,8 @@ const BookingPage = ({
const { uid } = responseData[0] || {}; const { uid } = responseData[0] || {};
return router.push({ return router.push({
pathname: "/success", pathname: `/booking/${uid}`,
query: { query: {
uid,
allRemainingBookings: true, allRemainingBookings: true,
email: bookingForm.getValues("email"), email: bookingForm.getValues("email"),
eventTypeSlug: eventType.slug, eventTypeSlug: eventType.slug,
@@ -237,7 +240,7 @@ const BookingPage = ({
const bookingFormSchema = z const bookingFormSchema = z
.object({ .object({
name: z.string().min(1), name: z.string().min(1),
email: z.string().email(), email: z.string().trim().email(),
phone: z phone: z
.string() .string()
.refine((val) => isValidPhoneNumber(val)) .refine((val) => isValidPhoneNumber(val))
@@ -318,7 +321,7 @@ const BookingPage = ({
const recurringBookings = recurringDates.map((recurringDate) => ({ const recurringBookings = recurringDates.map((recurringDate) => ({
...booking, ...booking,
start: dayjs(recurringDate).format(), start: dayjs(recurringDate).format(),
end: dayjs(recurringDate).add(eventType.length, "minute").format(), end: dayjs(recurringDate).add(duration, "minute").format(),
eventTypeId: eventType.id, eventTypeId: eventType.id,
eventTypeSlug: eventType.slug, eventTypeSlug: eventType.slug,
recurringEventId, recurringEventId,
@@ -351,7 +354,7 @@ const BookingPage = ({
mutation.mutate({ mutation.mutate({
...booking, ...booking,
start: dayjs(date).format(), start: dayjs(date).format(),
end: dayjs(date).add(eventType.length, "minute").format(), end: dayjs(date).add(duration, "minute").format(),
eventTypeId: eventType.id, eventTypeId: eventType.id,
eventTypeSlug: eventType.slug, eventTypeSlug: eventType.slug,
timeZone: timeZone(), timeZone: timeZone(),
@@ -426,7 +429,7 @@ const BookingPage = ({
eventTypeTitle: eventType.title, eventTypeTitle: eventType.title,
profileName: profile.name, profileName: profile.name,
})}{" "} })}{" "}
| Cal.com | {APP_NAME}
</title> </title>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
@@ -741,6 +744,27 @@ const BookingPage = ({
</div> </div>
</div> </div>
)} )}
{input.options && input.type === EventTypeCustomInputType.RADIO && (
<div className="">
<div className="flex">
<Group
onValueChange={(e) => {
bookingForm.setValue(`customInputs.${input.id}`, e);
}}>
<>
{input.options.map((option, i) => (
<RadioField
label={option.label}
key={`option.${i}.radio`}
value={option.label}
id={`option.${i}.radio`}
/>
))}
</>
</Group>
</div>
</div>
)}
</div> </div>
))} ))}
{!eventType.disableGuests && guestToggle && ( {!eventType.disableGuests && guestToggle && (
@@ -839,7 +863,7 @@ const BookingPage = ({
) : ( ) : (
<textarea <textarea
{...bookingForm.register("notes")} {...bookingForm.register("notes")}
required={!!eventType.metadata.additionalNotesRequired} required={!!eventType.metadata?.additionalNotesRequired}
id="notes" id="notes"
name="notes" name="notes"
rows={3} rows={3}
@@ -212,7 +212,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
return ( return (
<Dialog open={isOpenDialog}> <Dialog open={isOpenDialog}>
<DialogContent useOwnActionButtons> <DialogContent>
<div className="flex flex-row space-x-3"> <div className="flex flex-row space-x-3">
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10"> <div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
<Icon.FiMapPin className="text-primary-600 h-6 w-6" /> <Icon.FiMapPin className="text-primary-600 h-6 w-6" />
@@ -297,7 +297,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
control={locationFormMethods.control} control={locationFormMethods.control}
render={() => ( render={() => (
<Select<{ label: string; value: string; icon?: string }> <Select<{ label: string; value: string; icon?: string }>
maxMenuHeight={150} maxMenuHeight={300}
name="location" name="location"
defaultValue={selection} defaultValue={selection}
options={locationOptions} options={locationOptions}
@@ -47,8 +47,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
</div> </div>
<div className="pt-1"> <div className="pt-1">
<DialogHeader title={t("send_reschedule_request")} /> <DialogHeader title={t("send_reschedule_request")} />
<p className="text-sm text-gray-500">{t("reschedule_modal_description")}</p>
<p className="-mt-8 text-sm text-gray-500">{t("reschedule_modal_description")}</p>
<p className="mt-6 mb-2 text-sm font-bold text-black"> <p className="mt-6 mb-2 text-sm font-bold text-black">
{t("reason_for_reschedule_request")} {t("reason_for_reschedule_request")}
<span className="font-normal text-gray-500"> (Optional)</span> <span className="font-normal text-gray-500"> (Optional)</span>
@@ -62,9 +61,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
/> />
<DialogFooter> <DialogFooter>
<DialogClose asChild> <DialogClose />
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
<Button <Button
data-testid="send_request" data-testid="send_request"
disabled={isLoading} disabled={isLoading}
@@ -20,12 +20,13 @@ type AvailabilityOption = {
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => { const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
const { label, isDefault } = props.data; const { label, isDefault } = props.data;
const { t } = useLocale();
return ( return (
<components.Option {...props}> <components.Option {...props}>
<span>{label}</span> <span>{label}</span>
{isDefault && ( {isDefault && (
<Badge variant="blue" className="ml-2"> <Badge variant="blue" className="ml-2">
Default {t("default")}
</Badge> </Badge>
)} )}
</components.Option> </components.Option>
@@ -34,12 +35,13 @@ const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => { const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
const { label, isDefault } = props.data; const { label, isDefault } = props.data;
const { t } = useLocale();
return ( return (
<components.SingleValue {...props}> <components.SingleValue {...props}>
<span>{label}</span> <span>{label}</span>
{isDefault && ( {isDefault && (
<Badge variant="blue" className="ml-2"> <Badge variant="blue" className="ml-2">
Default {t("default")}
</Badge> </Badge>
)} )}
</components.SingleValue> </components.SingleValue>
@@ -57,6 +59,8 @@ const AvailabilitySelect = ({
onChange: (value: AvailabilityOption | null) => void; onChange: (value: AvailabilityOption | null) => void;
}) => { }) => {
const { data, isLoading } = trpc.viewer.availability.list.useQuery(); const { data, isLoading } = trpc.viewer.availability.list.useQuery();
const { t } = useLocale();
if (isLoading) { if (isLoading) {
return <SelectSkeletonLoader />; return <SelectSkeletonLoader />;
} }
@@ -77,6 +81,7 @@ const AvailabilitySelect = ({
return ( return (
<Select <Select
placeholder={t("select")}
options={options} options={options}
isSearchable={false} isSearchable={false}
onChange={props.onChange} onChange={props.onChange}
@@ -1,352 +0,0 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { SchedulingType } from "@prisma/client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import type { z } from "zod";
import classNames from "@calcom/lib/classNames";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
import { trpc } from "@calcom/trpc/react";
import {
Alert,
Button,
Dialog,
DialogClose,
DialogContent,
Dropdown,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
Form,
Icon,
InputLeading,
showToast,
TextAreaField,
TextField,
} from "@calcom/ui";
import { HttpError } from "@lib/core/http/error";
import { slugify } from "@lib/slugify";
import Avatar from "@components/ui/Avatar";
import * as RadioArea from "@components/ui/form/radio-area";
// this describes the uniform data needed to create a new event type on Profile or Team
export interface EventTypeParent {
teamId: number | null | undefined; // if undefined, then it's a profile
name?: string | null;
slug?: string | null;
image?: string | null;
}
interface CreateEventTypeBtnProps {
// set true for use on the team settings page
canAddEvents: boolean;
// set true when in use on the team settings page
isIndividualTeam?: boolean;
// EventTypeParent can be a profile (as first option) or a team for the rest.
options: EventTypeParent[];
}
export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
const { t, isLocaleReady } = useLocale();
const router = useRouter();
// URL encoded params
const teamId: number | undefined =
typeof router.query.teamId === "string" && router.query.teamId
? parseInt(router.query.teamId)
: undefined;
const pageSlug = router.query.eventPage || props.options[0].slug;
const hasTeams = !!props.options.find((option) => option.teamId);
const type: string = typeof router.query.type == "string" && router.query.type ? router.query.type : "";
const form = useForm<z.infer<typeof createEventTypeInput>>({
resolver: zodResolver(createEventTypeInput),
});
const { setValue, watch, register } = form;
useEffect(() => {
if (!router.isReady) return;
const title: string =
typeof router.query.title === "string" && router.query.title ? router.query.title : "";
const length: number =
typeof router.query.length === "string" && router.query.length ? parseInt(router.query.length) : 15;
const description: string =
typeof router.query.description === "string" && router.query.description
? router.query.description
: "";
const slug: string = typeof router.query.slug === "string" && router.query.slug ? router.query.slug : "";
setValue("title", title);
setValue("length", length);
setValue("description", description);
setValue("slug", slug);
// If query params change, update the form
}, [router.isReady, router.query, setValue]);
useEffect(() => {
const subscription = watch((value, { name, type }) => {
if (name === "title" && type === "change") {
if (value.title) setValue("slug", slugify(value.title));
else setValue("slug", "");
}
});
return () => subscription.unsubscribe();
}, [watch, setValue]);
const createMutation = trpc.viewer.eventTypes.create.useMutation({
onSuccess: async ({ eventType }) => {
await router.replace("/event-types/" + eventType.id);
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
}
if (err.data?.code === "BAD_REQUEST") {
const message = `${err.data.code}: URL already exists.`;
showToast(message, "error");
}
if (err.data?.code === "UNAUTHORIZED") {
const message = `${err.data.code}: You are not able to create this event`;
showToast(message, "error");
}
},
});
// inject selection data into url for correct router history
const openModal = (option: EventTypeParent) => {
const query = {
...router.query,
dialog: "new-eventtype",
eventPage: option.slug,
teamId: option.teamId,
};
if (!option.teamId) {
delete query.teamId;
}
router.push(
{
pathname: router.pathname,
query,
},
undefined,
{ shallow: true }
);
};
if (!isLocaleReady) {
return null;
}
return (
<Dialog
name="new-eventtype"
clearQueryParamsOnClose={["eventPage", "teamId", "type", "description", "title", "length", "slug"]}>
<CreateEventTypeTrigger
hasTeams={hasTeams}
canAddEvents={props.canAddEvents}
isIndividualTeam={props.isIndividualTeam}
openModal={openModal}
options={props.options}
/>
<DialogContent>
<div className="mb-4">
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
{teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
</h3>
<div>
<p className="text-sm text-gray-500">{t("new_event_type_to_book_description")}</p>
</div>
</div>
<Form
form={form}
handleSubmit={(values) => {
createMutation.mutate(values);
}}>
<div className="mt-3 space-y-4">
{teamId && (
<TextField
type="hidden"
labelProps={{ style: { display: "none" } }}
{...register("teamId", { valueAsNumber: true })}
value={teamId}
/>
)}
<TextField label={t("title")} placeholder={t("quick_chat")} {...register("title")} />
{process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined &&
process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? (
<TextField
label={`${t("url")}: ${process.env.NEXT_PUBLIC_WEBSITE_URL}`}
required
addOnLeading={<InputLeading>/{pageSlug}/</InputLeading>}
{...register("slug")}
/>
) : (
<TextField
label={t("url")}
required
addOnLeading={
<InputLeading>
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/
</InputLeading>
}
{...register("slug")}
/>
)}
<TextAreaField
label={t("description")}
placeholder={t("quick_video_meeting")}
{...register("description")}
/>
<div className="relative">
<TextField
type="number"
required
min="10"
placeholder="15"
label={t("length")}
className="pr-20"
{...register("length", { valueAsNumber: true })}
/>
<div className="absolute inset-y-0 right-0 mt-1.5 flex items-center pt-4 pr-3 text-sm text-gray-400">
{t("minutes")}
</div>
</div>
{teamId && (
<div className="mb-4">
<label htmlFor="schedulingType" className="block text-sm font-bold text-gray-700">
{t("scheduling_type")}
</label>
{form.formState.errors.schedulingType && (
<Alert
className="mt-1"
severity="error"
message={form.formState.errors.schedulingType.message}
/>
)}
<RadioArea.Group
{...register("schedulingType")}
onChange={(val) => form.setValue("schedulingType", val as SchedulingType)}
className="relative mt-1 flex space-x-6 rounded-sm rtl:space-x-reverse">
<RadioArea.Item
value={SchedulingType.COLLECTIVE}
defaultChecked={type === SchedulingType.COLLECTIVE}
className="w-1/2 text-sm">
<strong className="mb-1 block">{t("collective")}</strong>
<p>{t("collective_description")}</p>
</RadioArea.Item>
<RadioArea.Item
value={SchedulingType.ROUND_ROBIN}
defaultChecked={type === SchedulingType.ROUND_ROBIN}
className="w-1/2 text-sm">
<strong className="mb-1 block">{t("round_robin")}</strong>
<p>{t("round_robin_description")}</p>
</RadioArea.Item>
</RadioArea.Group>
</div>
)}
</div>
<div className="mt-8 flex flex-row-reverse gap-x-2">
<Button type="submit" loading={createMutation.isLoading}>
{t("continue")}
</Button>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
</div>
</Form>
</DialogContent>
</Dialog>
);
}
type CreateEventTypeTrigger = {
isIndividualTeam?: boolean;
// EventTypeParent can be a profile (as first option) or a team for the rest.
options: EventTypeParent[];
hasTeams: boolean;
// set true for use on the team settings page
canAddEvents: boolean;
openModal: (option: EventTypeParent) => void;
};
export function CreateEventTypeTrigger(props: CreateEventTypeTrigger) {
const { t } = useLocale();
return (
<>
{!props.hasTeams || props.isIndividualTeam ? (
<Button
onClick={() => props.openModal(props.options[0])}
data-testid="new-event-type"
StartIcon={Icon.FiPlus}
disabled={!props.canAddEvents}>
{t("new_event_type_btn")}
</Button>
) : (
<Dropdown>
<DropdownMenuTrigger asChild>
<Button EndIcon={Icon.FiChevronDown}>{t("new_event_type_btn")}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{t("new_event_subtitle")}</DropdownMenuLabel>
<DropdownMenuSeparator className="h-px bg-gray-200" />
{props.options.map((option) => (
<CreateEventTeamsItem
key={option.slug}
option={option}
openModal={() => props.openModal(option)}
/>
))}
</DropdownMenuContent>
</Dropdown>
)}
</>
);
}
function CreateEventTeamsItem(props: {
openModal: (option: EventTypeParent) => void;
option: EventTypeParent;
}) {
const session = useSession();
const membershipQuery = trpc.viewer.teams.getMembershipbyUser.useQuery({
memberId: session.data?.user.id as number,
teamId: props.option.teamId as number,
});
const isDisabled = membershipQuery.data?.role === "MEMBER";
return (
<DropdownMenuItem
key={props.option.slug}
className={classNames(
"cursor-pointer px-3 py-2 focus:outline-none",
isDisabled ? "cursor-default !text-gray-300" : "hover:bg-neutral-100"
)}
disabled={isDisabled}
onSelect={() => props.openModal(props.option)}>
<Avatar
alt={props.option.name || ""}
imageSrc={props.option.image || WEBAPP_URL + "/" + props.option.slug + "/avatar.png"}
size={6}
className="inline ltr:mr-2 rtl:ml-2"
/>
{props.option.name ? props.option.name : props.option.slug}
</DropdownMenuItem>
);
}
@@ -1,23 +1,33 @@
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { EventTypeCustomInputType } from "@prisma/client";
import type { CustomInputParsed } from "pages/event-types/[type]";
import { FC } from "react"; import { FC } from "react";
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form"; import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form";
import { Button, Select, TextField } from "@calcom/ui"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, Label, Select, TextField } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale";
interface OptionTypeBase { interface OptionTypeBase {
label: string; label: string;
value: EventTypeCustomInputType; value: EventTypeCustomInputType;
options?: { label: string; type: string }[];
} }
interface Props { interface Props {
onSubmit: SubmitHandler<IFormInput>; onSubmit: (output: CustomInputParsed) => void;
onCancel: () => void; onCancel: () => void;
selectedCustomInput?: EventTypeCustomInput; selectedCustomInput?: CustomInputParsed;
} }
type IFormInput = EventTypeCustomInput; type IFormInput = CustomInputParsed;
/**
* Getting a random ID gives us the option to know WHICH field is changed
* when the user edits a custom field.
* This UUID is only used to check for changes in the UI and not the ID we use in the DB
* There is very very very slim chance that this will cause a collision
* */
const randomId = () => Math.floor(Math.random() * 1000000 + new Date().getTime());
const CustomInputTypeForm: FC<Props> = (props) => { const CustomInputTypeForm: FC<Props> = (props) => {
const { t } = useLocale(); const { t } = useLocale();
@@ -26,10 +36,22 @@ const CustomInputTypeForm: FC<Props> = (props) => {
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") }, { value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
{ value: EventTypeCustomInputType.NUMBER, label: t("number") }, { value: EventTypeCustomInputType.NUMBER, label: t("number") },
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") }, { value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
{
value: EventTypeCustomInputType.RADIO,
label: t("radio"),
},
]; ];
const { selectedCustomInput } = props; const { selectedCustomInput } = props;
const defaultValues = selectedCustomInput || { type: inputOptions[0].value };
const { register, control, handleSubmit } = useForm<IFormInput>({ const defaultValues = selectedCustomInput
? { ...selectedCustomInput, id: selectedCustomInput?.id || randomId() }
: {
id: randomId(),
type: EventTypeCustomInputType.TEXT,
};
const { register, control, getValues } = useForm<IFormInput>({
defaultValues, defaultValues,
}); });
const selectedInputType = useWatch({ name: "type", control }); const selectedInputType = useWatch({ name: "type", control });
@@ -40,7 +62,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
}; };
return ( return (
<form onSubmit={handleSubmit(props.onSubmit)} className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<div> <div>
<label htmlFor="type" className="block text-sm font-medium text-gray-700"> <label htmlFor="type" className="block text-sm font-medium text-gray-700">
{t("input_type")} {t("input_type")}
@@ -83,6 +105,10 @@ const CustomInputTypeForm: FC<Props> = (props) => {
{...register("placeholder")} {...register("placeholder")}
/> />
)} )}
{selectedInputType === EventTypeCustomInputType.RADIO && (
<RadioInputHandler control={control} register={register} />
)}
<div className="flex h-5 items-center"> <div className="flex h-5 items-center">
<input <input
id="required" id="required"
@@ -111,10 +137,75 @@ const CustomInputTypeForm: FC<Props> = (props) => {
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2"> <Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
{t("cancel")} {t("cancel")}
</Button> </Button>
<Button type="submit">{t("save")}</Button> <Button
type="button"
onClick={() => {
props.onSubmit(getValues());
}}>
{t("save")}
</Button>
</div> </div>
</form> </div>
); );
}; };
function RadioInputHandler({
register,
control,
}: {
register: UseFormRegister<IFormInput>;
control: Control<IFormInput>;
}) {
const { t } = useLocale();
const { fields, append, remove } = useFieldArray<IFormInput>({
control,
name: "options",
shouldUnregister: true,
});
const [animateRef] = useAutoAnimate<HTMLUListElement>();
return (
<div className="flex flex-col ">
<Label htmlFor="radio_options">{t("options")}</Label>
<ul
className="flex max-h-80 w-full flex-col space-y-1 overflow-y-scroll rounded-md bg-gray-50 p-4"
ref={animateRef}>
<>
{fields.map((option, index) => (
<li key={`${option.id}`}>
<TextField
id={option.id}
placeholder={t("enter_option", { index: index + 1 })}
addOnFilled={false}
label={t("option", { index: index + 1 })}
labelSrOnly
{...register(`options.${index}.label` as const, { required: true })}
addOnSuffix={
<Button
size="icon"
color="minimal"
StartIcon={Icon.FiX}
onClick={() => {
remove(index);
}}
/>
}
/>
</li>
))}
<Button
color="minimal"
StartIcon={Icon.FiPlus}
className="!text-sm !font-medium"
onClick={() => {
append({ label: "", type: "text" });
}}>
{t("add_an_option")}
</Button>
</>
</ul>
</div>
);
}
export default CustomInputTypeForm; export default CustomInputTypeForm;
@@ -1,372 +0,0 @@
import { ErrorMessage } from "@hookform/error-message";
import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js";
import { Trans } from "next-i18next";
import { useEffect } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { components } from "react-select";
import { z } from "zod";
import {
EventLocationType,
getEventLocationType,
getHumanReadableLocationValue,
getMessageForOrganizer,
LocationObject,
LocationType,
} from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogContent, Form, Icon, Label, PhoneInput, Select } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
import { LinkText } from "@components/ui/LinkText";
import CheckboxField from "@components/ui/form/CheckboxField";
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
type OptionTypeBase = {
label: string;
value: EventLocationType["type"];
disabled?: boolean;
};
interface ISetLocationDialog {
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
selection?: OptionTypeBase;
booking?: BookingItem;
defaultValues?: LocationObject[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean;
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
}
const LocationInput = (props: {
eventLocationType: EventLocationType;
locationFormMethods: ReturnType<typeof useForm>;
id: string;
required: boolean;
placeholder: string;
className?: string;
defaultValue?: string;
}): JSX.Element | null => {
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
if (eventLocationType?.organizerInputType === "text") {
return (
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
);
} else if (eventLocationType?.organizerInputType === "phone") {
return (
<PhoneInput
name={eventLocationType.variable}
control={locationFormMethods.control}
{...remainingProps}
/>
);
}
return null;
};
export const EditLocationDialog = (props: ISetLocationDialog) => {
const {
saveLocation,
selection,
booking,
setShowLocationModal,
isOpenDialog,
defaultValues,
setSelectedLocation,
} = props;
const { t } = useLocale();
const locationsQuery = trpc.viewer.locationOptions.useQuery();
useEffect(() => {
if (selection) {
locationFormMethods.setValue("locationType", selection?.value);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selection]);
const locationFormSchema = z.object({
locationType: z.string(),
phone: z.string().optional(),
locationAddress: z.string().optional(),
locationLink: z
.string()
.optional()
.superRefine((val, ctx) => {
if (
eventLocationType &&
!eventLocationType.default &&
eventLocationType.linkType === "static" &&
eventLocationType.urlRegExp
) {
const valid = z
.string()
.regex(new RegExp(eventLocationType.urlRegExp || ""))
.safeParse(val).success;
if (!valid) {
const sampleUrl = eventLocationType.organizerInputPlaceholder;
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid URL for ${eventLocationType.label}. ${
sampleUrl ? "Sample URL: " + sampleUrl : ""
}`,
});
}
return;
}
const valid = z.string().url().optional().safeParse(val).success;
if (!valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid URL`,
});
}
return;
}),
displayLocationPublicly: z.boolean().optional(),
locationPhoneNumber: z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional(),
});
const locationFormMethods = useForm({
mode: "onSubmit",
resolver: zodResolver(locationFormSchema),
});
const selectedLocation = useWatch({
control: locationFormMethods.control,
name: "locationType",
});
const eventLocationType = getEventLocationType(selectedLocation);
const defaultLocation = defaultValues?.find(
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
);
const LocationOptions = (() => {
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
if (!eventLocationType.variable) {
console.error("eventLocationType.variable can't be undefined");
return null;
}
return (
<div>
<div>
<Label htmlFor="locationInput">{t(eventLocationType.messageForOrganizer || "")}</Label>
<LocationInput
locationFormMethods={locationFormMethods}
eventLocationType={eventLocationType}
id="locationInput"
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
required
className="block w-full rounded-md border-gray-300 text-sm"
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
defaultValue={
(defaultLocation && defaultLocation[eventLocationType.defaultValueVariable]) || ""
}
/>
<ErrorMessage
errors={locationFormMethods.formState.errors}
name={eventLocationType.variable}
className="mt-1 text-sm text-red-500"
as="p"
/>
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
);
} else {
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
}
})();
return (
<Dialog open={isOpenDialog}>
<DialogContent
type="creation"
Icon={Icon.FiMapPin}
useOwnActionButtons
title={t("edit_location")}
description={!booking ? t("this_input_will_shown_booking_this_event") : undefined}>
{booking && (
<>
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
<p className="mb-2 ml-1 text-sm text-black">
{getHumanReadableLocationValue(booking.location, t)}
</p>
</>
)}
<Form
form={locationFormMethods}
className="space-y-4"
handleSubmit={async (values) => {
const { locationType: newLocation, displayLocationPublicly } = values;
let details = {};
if (newLocation === LocationType.AttendeeInPerson) {
details = {
address: values.locationAddress,
};
}
if (newLocation === LocationType.InPerson) {
details = {
address: values.locationAddress,
};
}
const eventLocationType = getEventLocationType(newLocation);
// TODO: There can be a property that tells if it is to be saved in `link`
if (
newLocation === LocationType.Link ||
(!eventLocationType?.default && eventLocationType?.linkType === "static")
) {
details = { link: values.locationLink };
}
if (newLocation === LocationType.UserPhone) {
details = { hostPhoneNumber: values.locationPhoneNumber };
}
if (eventLocationType?.organizerInputType) {
details = {
...details,
displayLocationPublicly,
};
}
saveLocation(newLocation, details);
setShowLocationModal(false);
setSelectedLocation?.(undefined);
locationFormMethods.unregister([
"locationType",
"locationLink",
"locationAddress",
"locationPhoneNumber",
]);
}}>
<QueryCell
query={locationsQuery}
success={({ data: locationOptions }) => {
if (!locationOptions.length) return null;
if (booking) {
locationOptions.forEach((location) => {
if (location.label === "phone") {
location.options.filter((l) => l.value !== "phone");
}
});
}
return (
<Controller
name="locationType"
control={locationFormMethods.control}
render={() => (
<Select<{ label: string; value: string; icon?: string }>
maxMenuHeight={150}
name="location"
defaultValue={selection}
options={locationOptions}
components={{
Option: (props) => (
<components.Option {...props}>
<div className="flex items-center gap-3">
{props.data.icon && (
<img src={props.data.icon} alt="cover" className="h-3.5 w-3.5" />
)}
<span
className={classNames(
"text-sm font-medium",
props.isSelected ? "text-white" : "text-gray-900"
)}>
{props.data.label}
</span>
</div>
</components.Option>
),
}}
formatOptionLabel={(e) => (
<div className="flex items-center gap-2.5">
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
<span>{e.label}</span>
</div>
)}
formatGroupLabel={(e) => <p className="text-xs font-medium text-gray-600">{e.label}</p>}
isSearchable={false}
onChange={(val) => {
if (val) {
locationFormMethods.setValue("locationType", val.value);
locationFormMethods.unregister([
"locationLink",
"locationAddress",
"locationPhoneNumber",
]);
locationFormMethods.clearErrors([
"locationLink",
"locationPhoneNumber",
"locationAddress",
]);
setSelectedLocation?.(val);
}
}}
/>
)}
/>
);
}}
/>
{selectedLocation && LocationOptions}
<div className="mt-5">
<p className="text-sm text-gray-400">
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
Can&apos;t find the right video app? Visit our
<LinkText href="/apps/categories/video" classNameChildren="text-blue-400" target="_blank">
App Store
</LinkText>
.
</Trans>
</p>
</div>
<div className="mt-4 flex justify-end space-x-2">
<Button
onClick={() => {
setShowLocationModal(false);
setSelectedLocation?.(undefined);
locationFormMethods.unregister("locationType");
}}
type="button"
color="secondary">
{t("cancel")}
</Button>
<Button type="submit">{t("update")}</Button>
</div>
</Form>
</DialogContent>
</Dialog>
);
};
@@ -1,13 +1,12 @@
import { EventTypeCustomInput } from "@prisma/client/";
import Link from "next/link"; import Link from "next/link";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]"; import type { CustomInputParsed, EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import short from "short-uuid"; import short from "short-uuid";
import { v5 as uuidv5 } from "uuid"; import { v5 as uuidv5 } from "uuid";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector"; import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { CAL_URL } from "@calcom/lib/constants"; import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { import {
@@ -15,7 +14,9 @@ import {
Checkbox, Checkbox,
CustomInputItem, CustomInputItem,
Dialog, Dialog,
DialogClose,
DialogContent, DialogContent,
DialogFooter,
Icon, Icon,
Label, Label,
SettingsToggle, SettingsToggle,
@@ -26,6 +27,8 @@ import {
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm"; import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
import RequiresConfirmationController from "./RequiresConfirmationController";
const generateHashedLink = (id: number) => { const generateHashedLink = (id: number) => {
const translator = short(); const translator = short();
const seed = `${id}:${new Date().getTime()}`; const seed = `${id}:${new Date().getTime()}`;
@@ -41,11 +44,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink); const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl); const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link); const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>( const [customInputs, setCustomInputs] = useState<CustomInputParsed[]>(
eventType.customInputs.sort((a, b) => a.id - b.id) || [] eventType.customInputs.sort((a, b) => a.id - b.id) || []
); );
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined); const [selectedCustomInput, setSelectedCustomInput] = useState<CustomInputParsed | undefined>(undefined);
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false); const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`; const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled"); const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
@@ -99,7 +103,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<TextField <TextField
label={t("event_name")} label={t("event_name")}
type="text" type="text"
placeholder={t("meeting_with_user")} placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
defaultValue={eventType.eventName || ""} defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")} {...formMethods.register("eventName")}
addOnSuffix={ addOnSuffix={
@@ -123,15 +127,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
onCheckedChange={(e) => { onCheckedChange={(e) => {
if (e && customInputs.length === 0) { if (e && customInputs.length === 0) {
// Push a placeholders // Push a placeholders
setSelectedCustomInput(undefined);
setSelectedCustomInputModalOpen(true); setSelectedCustomInputModalOpen(true);
} else if (!e) { } else if (!e) {
setCustomInputs([]);
formMethods.setValue("customInputs", []); formMethods.setValue("customInputs", []);
} }
}}> }}>
<ul className="my-4"> <ul className="my-4 rounded-md border">
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => ( {customInputs.map((customInput, idx) => (
<CustomInputItem <CustomInputItem
key={idx} key={idx}
question={customInput.label} question={customInput.label}
@@ -154,24 +156,17 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
setSelectedCustomInput(undefined); setSelectedCustomInput(undefined);
setSelectedCustomInputModalOpen(true); setSelectedCustomInputModalOpen(true);
}}> }}>
Add an input {t("add_input")}
</Button> </Button>
)} )}
</SettingsToggle> </SettingsToggle>
</div> </div>
<hr /> <hr />
<Controller <RequiresConfirmationController
name="requiresConfirmation" seatsEnabled={seatsEnabled}
defaultValue={eventType.requiresConfirmation} metadata={eventType.metadata}
render={({ field: { value, onChange } }) => ( requiresConfirmation={requiresConfirmation}
<SettingsToggle onRequiresConfirmation={setRequiresConfirmation}
title={t("requires_confirmation")}
description={t("requires_confirmation_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
disabled={seatsEnabled}
/>
)}
/> />
<hr /> <hr />
<Controller <Controller
@@ -254,7 +249,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<SettingsToggle <SettingsToggle
data-testid="hashedLinkCheck" data-testid="hashedLinkCheck"
title={t("private_link")} title={t("private_link")}
description={t("private_link_description")} description={t("private_link_description", { appName: APP_NAME })}
checked={hashedLinkVisible} checked={hashedLinkVisible}
onCheckedChange={(e) => { onCheckedChange={(e) => {
formMethods.setValue("hashedLink", e ? hashedUrl : undefined); formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
@@ -307,6 +302,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
if (e) { if (e) {
formMethods.setValue("disableGuests", true); formMethods.setValue("disableGuests", true);
formMethods.setValue("requiresConfirmation", false); formMethods.setValue("requiresConfirmation", false);
setRequiresConfirmation(false);
formMethods.setValue("seatsPerTimeSlot", 2); formMethods.setValue("seatsPerTimeSlot", 2);
} else { } else {
formMethods.setValue("seatsPerTimeSlot", null); formMethods.setValue("seatsPerTimeSlot", null);
@@ -352,15 +348,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<DialogContent <DialogContent
title={t("custom_event_name")} title={t("custom_event_name")}
description={t("custom_event_name_description")} description={t("custom_event_name_description")}
type="creation" type="creation">
actionText="Create"
// Set event name back to what it was on close
actionOnClose={() => formMethods.setValue("eventName", eventType.eventName ?? "")}
actionOnClick={() => setShowEventNameTip(false)}>
<TextField <TextField
label={t("event_name")} label={t("event_name")}
type="text" type="text"
placeholder={t("meeting_with_user")} placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
defaultValue={eventType.eventName || ""} defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")} {...formMethods.register("eventName")}
/> />
@@ -370,6 +362,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<p>{`{HOST/ATTENDEE} = ${t("dynamically_display_attendee_or_organizer")}`}</p> <p>{`{HOST/ATTENDEE} = ${t("dynamically_display_attendee_or_organizer")}`}</p>
<p>{`{LOCATION} = ${t("event_location")}`}</p> <p>{`{LOCATION} = ${t("event_location")}`}</p>
</div> </div>
<DialogFooter>
<Button color="primary" onClick={() => setShowEventNameTip(false)}>
{t("create")}
</Button>
<DialogClose onClick={() => formMethods.setValue("eventName", eventType.eventName ?? "")} />
</DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}
@@ -383,29 +381,40 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
type="creation" type="creation"
Icon={Icon.FiPlus} Icon={Icon.FiPlus}
title={t("add_new_custom_input_field")} title={t("add_new_custom_input_field")}
useOwnActionButtons
description={t("this_input_will_shown_booking_this_event")}> description={t("this_input_will_shown_booking_this_event")}>
<CustomInputTypeForm <CustomInputTypeForm
selectedCustomInput={selectedCustomInput} selectedCustomInput={selectedCustomInput}
onSubmit={(values) => { onSubmit={(values) => {
const customInput: EventTypeCustomInput = { const customInput: CustomInputParsed = {
id: -1, id: -1,
eventTypeId: -1, eventTypeId: -1,
label: values.label, label: values.label,
placeholder: values.placeholder, placeholder: values.placeholder,
required: values.required, required: values.required,
type: values.type, type: values.type,
options: values.options,
}; };
if (selectedCustomInput) { if (selectedCustomInput) {
selectedCustomInput.label = customInput.label; selectedCustomInput.label = customInput.label;
selectedCustomInput.placeholder = customInput.placeholder; selectedCustomInput.placeholder = customInput.placeholder;
selectedCustomInput.required = customInput.required; selectedCustomInput.required = customInput.required;
selectedCustomInput.type = customInput.type; selectedCustomInput.type = customInput.type;
selectedCustomInput.options = customInput.options || undefined;
// Update by id
const inputIndex = customInputs.findIndex((input) => input.id === values.id);
customInputs[inputIndex] = selectedCustomInput;
setCustomInputs(customInputs);
formMethods.setValue("customInputs", customInputs);
} else { } else {
setCustomInputs(customInputs.concat(customInput)); const concatted = customInputs.concat({
formMethods.setValue("customInputs", customInputs.concat(customInput)); ...customInput,
options: customInput.options,
});
console.log(concatted);
setCustomInputs(concatted);
formMethods.setValue("customInputs", concatted);
} }
setSelectedCustomInputModalOpen(false); setSelectedCustomInputModalOpen(false);
}} }}
onCancel={() => { onCancel={() => {
+105 -13
View File
@@ -4,12 +4,13 @@ import { isValidPhoneNumber } from "libphonenumber-js";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]"; import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useState } from "react"; import { useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form"; import { Controller, useForm, useFormContext } from "react-hook-form";
import { MultiValue } from "react-select";
import { z } from "zod"; import { z } from "zod";
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations"; import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, Label, Select, Skeleton, TextField } from "@calcom/ui"; import { Button, Icon, Label, Select, Skeleton, TextField, SettingsToggle } from "@calcom/ui";
import { slugify } from "@lib/slugify"; import { slugify } from "@lib/slugify";
@@ -30,6 +31,22 @@ export const EventSetupTab = (
const [showLocationModal, setShowLocationModal] = useState(false); const [showLocationModal, setShowLocationModal] = useState(false);
const [editingLocationType, setEditingLocationType] = useState<string>(""); const [editingLocationType, setEditingLocationType] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined); const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
value: mins,
label: t("multiple_duration_mins", { count: mins }),
}));
const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
MultiValue<{
value: number;
label: string;
}>
>(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
const [defaultDuration, setDefaultDuration] = useState(
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
);
const openLocationModal = (type: EventLocationType["type"]) => { const openLocationModal = (type: EventLocationType["type"]) => {
setSelectedLocation(locationOptions.find((option) => option.value === type)); setSelectedLocation(locationOptions.find((option) => option.value === type));
@@ -113,6 +130,7 @@ export const EventSetupTab = (
{validLocations.length === 0 && ( {validLocations.length === 0 && (
<div className="flex"> <div className="flex">
<Select <Select
placeholder={t("select")}
options={locationOptions} options={locationOptions}
isSearchable={false} isSearchable={false}
className="block w-full min-w-0 flex-1 rounded-sm text-sm" className="block w-full min-w-0 flex-1 rounded-sm text-sm"
@@ -195,7 +213,7 @@ export const EventSetupTab = (
<div className="space-y-8"> <div className="space-y-8">
<TextField <TextField
required required
label={t("Title")} label={t("title")}
defaultValue={eventType.title} defaultValue={eventType.title}
{...formMethods.register("title")} {...formMethods.register("title")}
/> />
@@ -219,17 +237,91 @@ export const EventSetupTab = (
setValueAs: (v) => slugify(v), setValueAs: (v) => slugify(v),
})} })}
/> />
<TextField {multipleDuration ? (
required <div className="space-y-4">
name="length" <div>
type="number" <Skeleton as={Label} loadingClassName="w-16">
label={t("duration")} {t("available_durations")}
addOnSuffix={<>{t("minutes")}</>} </Skeleton>
defaultValue={eventType.length ?? 15} <Select
onChange={(e) => { isMulti
formMethods.setValue("length", Number(e.target.value)); defaultValue={selectedMultipleDuration}
}} name="metadata.multipleDuration"
/> isSearchable={false}
className="h-auto !min-h-[36px] text-sm"
options={multipleDurationOptions}
onChange={(options) => {
const values = options
.map((opt) => opt.value)
.sort(function (a, b) {
return a - b;
});
setMultipleDuration(values);
setSelectedMultipleDuration(options);
if (!options.find((opt) => opt.value === defaultDuration?.value)) {
if (options.length > 0) {
setDefaultDuration(options[0]);
} else {
setDefaultDuration(null);
}
}
if (options.length === 1 && defaultDuration === null) {
setDefaultDuration(options[0]);
}
formMethods.setValue("metadata.multipleDuration", values);
}}
/>
</div>
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("default_duration")}
</Skeleton>
<Select
value={defaultDuration}
isSearchable={false}
name="length"
className="text-sm"
noOptionsMessage={() => t("default_duration_no_options")}
options={selectedMultipleDuration}
onChange={(option) => {
setDefaultDuration(
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
);
formMethods.setValue("length", Number(option?.value));
}}
/>
</div>
</div>
) : (
<TextField
required
name="length"
type="number"
label={t("duration")}
addOnSuffix={<>{t("minutes")}</>}
defaultValue={eventType.length ?? 15}
onChange={(e) => {
formMethods.setValue("length", Number(e.target.value));
}}
/>
)}
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
<SettingsToggle
title={t("allow_booker_to_select_duration")}
checked={multipleDuration !== undefined}
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
} else {
setMultipleDuration([]);
formMethods.setValue("metadata.multipleDuration", []);
formMethods.setValue("length", 0);
}
}}
/>
</div>
<div> <div>
<Skeleton as={Label} loadingClassName="w-16"> <Skeleton as={Label} loadingClassName="w-16">
{t("location")} {t("location")}
@@ -67,7 +67,7 @@ function getNavigation(props: {
name: "availability", name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`, href: `/event-types/${eventType.id}?tabName=availability`,
icon: Icon.FiCalendar, icon: Icon.FiCalendar,
info: `Working Hours`, // TODO: Get this from props info: `default_schedule_name`, // TODO: Get this from props
}, },
{ {
name: "event_limit_tab_title", name: "event_limit_tab_title",
@@ -0,0 +1,156 @@
import * as RadioGroup from "@radix-ui/react-radio-group";
import { UnitTypeLongPlural } from "dayjs";
import { Trans } from "next-i18next";
import type { FormValues } from "pages/event-types/[type]";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Alert, Input, Label, SettingsToggle } from "@calcom/ui";
type RequiresConfirmationControllerProps = {
metadata: z.infer<typeof EventTypeMetaDataSchema>;
requiresConfirmation: boolean;
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
seatsEnabled: boolean;
};
export default function RequiresConfirmationController({
metadata,
requiresConfirmation,
onRequiresConfirmation,
seatsEnabled,
}: RequiresConfirmationControllerProps) {
const { t } = useLocale();
const [requiresConfirmationSetup, setRequiresConfirmationSetup] = useState(
metadata?.requiresConfirmationThreshold
);
const defaultRequiresConfirmationSetup = { time: 30, unit: "minutes" as UnitTypeLongPlural };
const formMethods = useFormContext<FormValues>();
useEffect(() => {
if (!requiresConfirmation) {
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
}
}, [requiresConfirmation]);
return (
<div className="block items-start sm:flex">
<div className={!seatsEnabled ? "w-full" : ""}>
{seatsEnabled ? (
<Alert severity="warning" title="Seats option doesn't support confirmation requirement" />
) : (
<Controller
name="requiresConfirmation"
control={formMethods.control}
render={() => (
<SettingsToggle
title={t("requires_confirmation")}
description={t("requires_confirmation_description")}
checked={requiresConfirmation}
onCheckedChange={(val) => {
formMethods.setValue("requiresConfirmation", val);
onRequiresConfirmation(val);
}}>
<RadioGroup.Root
defaultValue={
requiresConfirmation
? requiresConfirmationSetup === undefined
? "always"
: "notice"
: undefined
}
onValueChange={(val) => {
if (val === "always") {
formMethods.setValue("requiresConfirmation", true);
onRequiresConfirmation(true);
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
setRequiresConfirmationSetup(undefined);
} else if (val === "notice") {
formMethods.setValue("requiresConfirmation", true);
onRequiresConfirmation(true);
formMethods.setValue(
"metadata.requiresConfirmationThreshold",
requiresConfirmationSetup || defaultRequiresConfirmationSetup
);
}
}}>
<div className="-ml-2 flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
<div className="flex items-center">
<RadioGroup.Item
id="always"
value="always"
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="always" className="!m-0 flex items-center">
{t("always_requires_confirmation")}
</Label>
</div>
<div className="flex items-center">
<RadioGroup.Item
id="notice"
value="notice"
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="notice" className="!m-0 flex items-center">
<Trans
i18nKey="when_booked_with_less_than_notice"
defaults="When booked with less than <time></time> notice"
components={{
time: (
<div className="mx-2 flex">
<Input
type="number"
min={1}
onChange={(evt) => {
const val = Number(evt.target?.value);
setRequiresConfirmationSetup({
unit:
requiresConfirmationSetup?.unit ??
defaultRequiresConfirmationSetup.unit,
time: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
}}
className="!m-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
/>
<select
onChange={(evt) => {
const val = evt.target.value as UnitTypeLongPlural;
setRequiresConfirmationSetup({
time:
requiresConfirmationSetup?.time ??
defaultRequiresConfirmationSetup.time,
unit: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.unit", val);
}}
className="ml-2 block h-9 rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
defaultValue={
metadata?.requiresConfirmationThreshold?.unit ||
defaultRequiresConfirmationSetup.unit
}>
<option value="minutes">{t("minute_timeUnit")}</option>
<option value="hours">{t("hour_timeUnit")}</option>
</select>
</div>
),
}}
/>
</Label>
</div>
</div>
</RadioGroup.Root>
</SettingsToggle>
)}
/>
)}
</div>
</div>
);
}
@@ -4,21 +4,20 @@ import { FormEvent, useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { User } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, ImageUploader, showToast, TextArea } from "@calcom/ui"; import { Button, ImageUploader, showToast, TextArea } from "@calcom/ui";
import { Avatar } from "@calcom/ui";
import { AvatarSSR } from "@components/ui/AvatarSSR"; import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
interface IUserProfile {
user?: User;
}
type FormData = { type FormData = {
bio: string; bio: string;
}; };
interface IUserProfileProps {
user: IOnboardingPageProps["user"];
}
const UserProfile = (props: IUserProfile) => { const UserProfile = (props: IUserProfileProps) => {
const { user } = props; const { user } = props;
const { t } = useLocale(); const { t } = useLocale();
const avatarRef = useRef<HTMLInputElement>(null!); const avatarRef = useRef<HTMLInputElement>(null!);
@@ -100,7 +99,14 @@ const UserProfile = (props: IUserProfile) => {
return ( return (
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<div className="flex flex-row items-center justify-start rtl:justify-end"> <div className="flex flex-row items-center justify-start rtl:justify-end">
{user && <AvatarSSR user={user} alt="Profile picture" className="h-16 w-16" />} {user && (
<Avatar
alt={user.username || "user avatar"}
gravatarFallbackMd5={user.emailMd5}
size="lg"
imageSrc={imageSrc}
/>
)}
<input <input
ref={avatarRef} ref={avatarRef}
type="hidden" type="hidden"
@@ -1,17 +1,18 @@
import { ArrowRightIcon } from "@heroicons/react/outline"; import { ArrowRightIcon } from "@heroicons/react/outline";
import { useState } from "react"; import { useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { User } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, TimezoneSelect } from "@calcom/ui"; import { Button, TimezoneSelect } from "@calcom/ui";
import { UsernameAvailability } from "@components/ui/UsernameAvailability"; import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
interface IUserSettingsProps { interface IUserSettingsProps {
user: User; user: IOnboardingPageProps["user"];
nextStep: () => void; nextStep: () => void;
} }
@@ -23,11 +24,9 @@ const UserSettings = (props: IUserSettingsProps) => {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
control,
} = useForm({ } = useForm({
defaultValues: { defaultValues: {
name: user?.name || "", name: user?.name || "",
username: user?.username || "",
}, },
reValidateMode: "onChange", reValidateMode: "onChange",
}); });
@@ -41,39 +40,19 @@ const UserSettings = (props: IUserSettingsProps) => {
const mutation = trpc.viewer.updateProfile.useMutation({ const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: onSuccess, onSuccess: onSuccess,
}); });
const { data: stripeCustomer } = trpc.viewer.stripeCustomer.useQuery();
const paymentRequired = stripeCustomer?.isPremium ? !stripeCustomer?.paidForPremium : false;
const onSubmit = handleSubmit((data) => { const onSubmit = handleSubmit((data) => {
if (paymentRequired) {
return;
}
mutation.mutate({ mutation.mutate({
name: data.name, name: data.name,
timeZone: selectedTimeZone, timeZone: selectedTimeZone,
}); });
}); });
const [currentUsername, setCurrentUsername] = useState(user.username || "");
return ( return (
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<div className="space-y-6"> <div className="space-y-6">
{/* Username textfield */} {/* Username textfield */}
<Controller <UsernameAvailabilityField user={user} />
control={control}
name="username"
render={({ field: { value, ref, onChange } }) => (
<UsernameAvailability
readonly={true}
currentUsername={currentUsername}
setCurrentUsername={setCurrentUsername}
inputUsernameValue={value}
usernameRef={ref}
setInputUsernameValue={onChange}
user={user}
/>
)}
/>
{/* Full name textfield */} {/* Full name textfield */}
<div className="w-full"> <div className="w-full">
@@ -1,134 +0,0 @@
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
import React, { FC } from "react";
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
import { Button } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale";
import Select from "@components/ui/form/Select";
interface OptionTypeBase {
label: string;
value: EventTypeCustomInputType;
}
interface Props {
onSubmit: SubmitHandler<IFormInput>;
onCancel: () => void;
selectedCustomInput?: EventTypeCustomInput;
}
type IFormInput = EventTypeCustomInput;
const CustomInputTypeForm: FC<Props> = (props) => {
const { t } = useLocale();
const inputOptions: OptionTypeBase[] = [
{ value: EventTypeCustomInputType.TEXT, label: t("text") },
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
];
const { selectedCustomInput } = props;
const defaultValues = selectedCustomInput || { type: inputOptions[0].value };
const { register, control, handleSubmit } = useForm<IFormInput>({
defaultValues,
});
const selectedInputType = useWatch({ name: "type", control });
const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value);
const onCancel = () => {
props.onCancel();
};
return (
<form onSubmit={handleSubmit(props.onSubmit)}>
<div className="mb-2">
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
{t("input_type")}
</label>
<Controller
name="type"
control={control}
render={({ field }) => (
<Select
id="type"
defaultValue={selectedInputOption}
options={inputOptions}
isSearchable={false}
className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm"
onChange={(option) => option && field.onChange(option.value)}
value={selectedInputOption}
onBlur={field.onBlur}
name={field.name}
/>
)}
/>
</div>
<div className="mb-2">
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
{t("label")}
</label>
<div className="mt-1">
<input
type="text"
id="label"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={selectedCustomInput?.label}
{...register("label", { required: true })}
/>
</div>
</div>
{(selectedInputType === EventTypeCustomInputType.TEXT ||
selectedInputType === EventTypeCustomInputType.TEXTLONG) && (
<div className="mb-2">
<label htmlFor="placeholder" className="block text-sm font-medium text-gray-700">
{t("placeholder")}
</label>
<div className="mt-1">
<input
type="text"
id="placeholder"
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={selectedCustomInput?.placeholder}
{...register("placeholder")}
/>
</div>
</div>
)}
<div className="flex h-5 items-center">
<input
id="required"
type="checkbox"
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300 ltr:mr-2 rtl:ml-2"
defaultChecked={selectedCustomInput?.required ?? true}
{...register("required")}
/>
<label htmlFor="required" className="block text-sm font-medium text-gray-700">
{t("is_required")}
</label>
</div>
<input
type="hidden"
id="eventTypeId"
value={selectedCustomInput?.eventTypeId || -1}
{...register("eventTypeId", { valueAsNumber: true })}
/>
<input
type="hidden"
id="id"
value={selectedCustomInput?.id || -1}
{...register("id", { valueAsNumber: true })}
/>
<div className="mt-5 flex space-x-2 sm:mt-4">
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
{t("cancel")}
</Button>
<Button type="submit">{t("save")}</Button>
</div>
</form>
);
};
export default CustomInputTypeForm;
@@ -0,0 +1,73 @@
import type { Attendee, Booking, User } from "@prisma/client";
import type { FC } from "react";
import { useMemo } from "react";
import { JsonLd } from "react-schemaorg";
import type { EventReservation, Person, ReservationStatusType } from "schema-dts";
type EventSchemaUser = Pick<User, "name" | "email">;
type EventSchemaAttendee = Pick<Attendee, "name" | "email">;
interface EventReservationSchemaInterface {
reservationId: Booking["uid"];
eventName: Booking["title"];
startTime: Booking["startTime"];
endTime: Booking["endTime"];
organizer: EventSchemaUser | null;
attendees: EventSchemaAttendee[];
location: Booking["location"];
description: Booking["description"];
status: Booking["status"];
}
const EventReservationSchema: FC<EventReservationSchemaInterface> = ({
reservationId,
eventName,
startTime,
endTime,
organizer,
attendees,
location,
description,
status,
}) => {
const reservationStatus = useMemo<ReservationStatusType>(() => {
switch (status) {
case "ACCEPTED":
return "ReservationConfirmed";
case "REJECTED":
case "CANCELLED":
return "ReservationCancelled";
case "PENDING":
return "ReservationPending";
default:
return "ReservationHold";
}
}, [status]);
return (
<JsonLd<EventReservation>
item={{
"@context": "https://schema.org",
"@type": "EventReservation",
reservationId,
reservationStatus,
reservationFor: {
"@type": "Event",
name: eventName,
startDate: startTime.toString(),
endDate: endTime.toString(),
organizer: organizer
? ({ "@type": "Person", name: organizer.name, email: organizer.email } as Person)
: undefined,
attendee: attendees?.map(
(person) => ({ "@type": "Person", name: person.name, email: person.email } as Person)
),
location: location || undefined,
description: description || undefined,
},
}}
/>
);
};
export default EventReservationSchema;
+2 -1
View File
@@ -9,6 +9,7 @@ import {
MeetingImageProps, MeetingImageProps,
} from "@calcom/lib/OgImages"; } from "@calcom/lib/OgImages";
import { getBrowserInfo } from "@calcom/lib/browser/browser.utils"; import { getBrowserInfo } from "@calcom/lib/browser/browser.utils";
import { APP_NAME } from "@calcom/lib/constants";
import { truncateOnWord } from "@calcom/lib/text"; import { truncateOnWord } from "@calcom/lib/text";
import { getSeoImage, seoConfig } from "@lib/config/next-seo.config"; import { getSeoImage, seoConfig } from "@lib/config/next-seo.config";
@@ -79,7 +80,7 @@ export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
const image = getSeoImage("ogImage") + constructGenericImage({ title, description }); const image = getSeoImage("ogImage") + constructGenericImage({ title, description });
const truncatedDescription = truncateOnWord(description, 158); const truncatedDescription = truncateOnWord(description, 158);
const pageTitle = title + " | Cal.com"; const pageTitle = title + " | " + APP_NAME;
let seoObject = buildSeoMeta({ let seoObject = buildSeoMeta({
title: pageTitle, title: pageTitle,
image, image,
@@ -72,11 +72,7 @@ const DisableTwoFactorAuthModal = ({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent <DialogContent title={t("disable_2fa")} description={t("disable_2fa_recommendation")} type="creation">
title={t("disable_2fa")}
description={t("disable_2fa_recommendation")}
type="creation"
useOwnActionButtons>
<Form form={form} handleSubmit={handleDisable}> <Form form={form} handleSubmit={handleDisable}>
<div className="mb-4"> <div className="mb-4">
<PasswordField <PasswordField
@@ -130,11 +130,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent <DialogContent title={t("enable_2fa")} description={setupDescriptions[step]} type="creation">
title={t("enable_2fa")}
description={setupDescriptions[step]}
type="creation"
useOwnActionButtons>
<WithStep step={SetupStep.ConfirmPassword} current={step}> <WithStep step={SetupStep.ConfirmPassword} current={step}>
<form onSubmit={handleSetup}> <form onSubmit={handleSetup}>
<div className="mb-4"> <div className="mb-4">
-207
View File
@@ -1,207 +0,0 @@
import React, { useRef, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { objectKeys } from "@calcom/lib/objectKeys";
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { trpc } from "@calcom/trpc/react";
import { Alert, Button, Icon, showToast, TextField } from "@calcom/ui";
import ImageUploader from "@components/ImageUploader";
import SettingInputContainer from "@components/ui/SettingInputContainer";
interface Props {
team: TeamWithMembers | null | undefined;
}
export default function TeamSettings(props: Props) {
const { t } = useLocale();
const [hasErrors, setHasErrors] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const team = props.team;
const hasLogo = !!team?.logo;
const utils = trpc.useContext();
const mutation = trpc.viewer.teams.update.useMutation({
onError: (err) => {
setHasErrors(true);
setErrorMessage(err.message);
},
async onSuccess() {
await utils.viewer.teams.get.invalidate();
showToast(t("your_team_updated_successfully"), "success");
setHasErrors(false);
},
});
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const teamUrlRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const descriptionRef = useRef<HTMLTextAreaElement>() as React.MutableRefObject<HTMLTextAreaElement>;
const hideBrandingRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const logoRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
function updateTeamData() {
if (!team) return;
const variables = {
name: nameRef.current?.value,
slug: teamUrlRef.current?.value,
bio: descriptionRef.current?.value,
hideBranding: hideBrandingRef.current?.checked,
};
// remove unchanged variables
objectKeys(variables).forEach((key) => {
if (variables[key as keyof typeof variables] === team?.[key]) delete variables[key];
});
mutation.mutate({ id: team.id, ...variables });
}
function updateLogo(newLogo: string) {
if (!team) return;
logoRef.current.value = newLogo;
mutation.mutate({ id: team.id, logo: newLogo });
}
const removeLogo = () => updateLogo("");
return (
<div className="divide-y divide-gray-200 lg:col-span-9">
<div className="">
{hasErrors && <Alert severity="error" title={errorMessage} />}
<form
className="divide-y divide-gray-200 lg:col-span-9"
onSubmit={(e) => {
e.preventDefault();
updateTeamData();
}}>
<div className="py-6">
<div className="flex flex-col lg:flex-row">
<div className="flex-grow space-y-6">
<SettingInputContainer
Icon={Icon.FiLink}
label="Team URL"
htmlFor="team-url"
Input={
<TextField
name="" // typescript requires name but we don't want component to render name label
id="team-url"
addOnLeading={
<span className="inline-flex items-center rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500">
{process.env.NEXT_PUBLIC_WEBSITE_URL}/team/
</span>
}
ref={teamUrlRef}
defaultValue={team?.slug as string}
/>
}
/>
<SettingInputContainer
Icon={Icon.FiHash}
label="Team Name"
htmlFor="name"
Input={
<input
ref={nameRef}
type="text"
name="name"
id="name"
placeholder={t("your_team_name")}
required
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm"
defaultValue={team?.name as string}
/>
}
/>
<hr />
<div>
<SettingInputContainer
Icon={Icon.FiInfo}
label={t("about")}
htmlFor="about"
Input={
<>
<textarea
ref={descriptionRef}
id="about"
name="about"
rows={3}
defaultValue={team?.bio as string}
className="mt-1 block w-full rounded-sm border-gray-300 text-sm"
/>
<p className="mt-2 text-sm text-gray-500">{t("team_description")}</p>
</>
}
/>
</div>
<div>
<SettingInputContainer
Icon={Icon.FiImage}
label="Logo"
htmlFor="avatar"
Input={
<>
<div className="mt-1 flex">
<input
ref={logoRef}
type="hidden"
name="avatar"
id="avatar"
placeholder="URL"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm"
defaultValue={team?.logo ?? undefined}
/>
<ImageUploader
target="logo"
id="logo-upload"
buttonMsg={hasLogo ? t("edit_logo") : t("upload_a_logo")}
handleAvatarChange={updateLogo}
imageSrc={team?.logo ?? undefined}
/>
{hasLogo && (
<Button
onClick={removeLogo}
color="secondary"
type="button"
className="ml-1 py-1 text-xs">
{t("remove_logo")}
</Button>
)}
</div>
</>
}
/>
<hr className="mt-6" />
</div>
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<input
id="hide-branding"
name="hide-branding"
type="checkbox"
ref={hideBrandingRef}
defaultChecked={team?.hideBranding}
className="h-4 w-4 rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-500"
/>
</div>
<div className="text-sm ltr:ml-3 rtl:mr-3">
<label htmlFor="hide-branding" className="font-medium text-gray-700">
{t("disable_cal_branding")}
</label>
<p className="text-gray-500">{t("disable_cal_branding_description")}</p>
</div>
</div>
</div>
</div>
</div>
<div className="flex justify-end py-4">
<Button type="submit" color="primary">
{t("save")}
</Button>
</div>
</form>
</div>
</div>
);
}
@@ -1,126 +0,0 @@
import { MembershipRole } from "@prisma/client";
import Link from "next/link";
import { useRouter } from "next/router";
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { trpc } from "@calcom/trpc/react";
import { ConfirmationDialogContent, Dialog, DialogTrigger, Icon, showToast } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale";
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
import LinkIconButton from "@components/ui/LinkIconButton";
export default function TeamSettingsRightSidebar(props: { team: TeamWithMembers; role: MembershipRole }) {
const { t } = useLocale();
const utils = trpc.useContext();
const router = useRouter();
const permalink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${props.team?.slug}`;
const deleteTeamMutation = trpc.viewer.teams.delete.useMutation({
async onSuccess() {
await utils.viewer.teams.get.invalidate();
router.push(`/settings/teams`);
showToast(t("your_team_updated_successfully"), "success");
},
});
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
onSuccess: () => {
utils.viewer.teams.list.invalidate();
router.push(`/settings/teams`);
},
});
function deleteTeam() {
if (props.team?.id) deleteTeamMutation.mutate({ teamId: props.team.id });
}
function leaveTeam() {
if (props.team?.id)
acceptOrLeaveMutation.mutate({
teamId: props.team.id,
accept: false,
});
}
return (
<div className="space-y-6 px-2">
{(props.role === MembershipRole.OWNER || props.role === MembershipRole.ADMIN) && (
<CreateEventTypeButton
isIndividualTeam
canAddEvents={true}
options={[
{
teamId: props.team?.id,
name: props.team?.name,
slug: props.team?.slug,
image: props.team?.logo,
},
]}
/>
)}
<div className="space-y-1">
<Link href={permalink} passHref={true}>
<a target="_blank">
<LinkIconButton Icon={Icon.FiExternalLink}>{t("preview")}</LinkIconButton>
</a>
</Link>
<LinkIconButton
Icon={Icon.FiLink}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Copied to clipboard", "success");
}}>
{t("copy_link_team")}
</LinkIconButton>
{props.role === "OWNER" ? (
<Dialog>
<DialogTrigger asChild>
<LinkIconButton
onClick={(e) => {
e.stopPropagation();
}}
Icon={Icon.FiTrash}>
{t("disband_team")}
</LinkIconButton>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("disband_team")}
confirmBtnText={t("confirm_disband_team")}
onConfirm={deleteTeam}>
{t("disband_team_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
) : (
<Dialog>
<DialogTrigger asChild>
<LinkIconButton
Icon={Icon.FiLogOut}
onClick={(e) => {
e.stopPropagation();
}}>
{t("leave_team")}
</LinkIconButton>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("leave_team")}
confirmBtnText={t("confirm_leave_team")}
onConfirm={leaveTeam}>
{t("leave_team_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
)}
</div>
{props.team?.id && props.role !== MembershipRole.MEMBER && (
<Link href={`/settings/teams/${props.team.id}/availability`}>
<div className="mt-5 hidden space-y-1 sm:block">
<LinkIconButton Icon={Icon.FiClock}>View Availability</LinkIconButton>
<p className="mt-2 text-sm text-gray-500">See your team members availability at a glance.</p>
</div>
</Link>
)}
</div>
);
}
+2 -2
View File
@@ -1,7 +1,7 @@
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { LOGO } from "@calcom/lib/constants"; import { APP_NAME, LOGO } from "@calcom/lib/constants";
import Loader from "@components/Loader"; import Loader from "@components/Loader";
import { HeadSeo } from "@components/seo/head-seo"; import { HeadSeo } from "@components/seo/head-seo";
@@ -21,7 +21,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
<HeadSeo title={props.title} description={props.description} /> <HeadSeo title={props.title} description={props.description} />
{props.showLogo && ( {props.showLogo && (
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
<img className="mb-auto h-4" src={LOGO} alt="Cal.com Logo" /> <img className="mb-auto h-4" src={LOGO} alt={`${APP_NAME} Logo`} />
)} )}
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}> <div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
{props.heading && <h2 className="font-cal text-center text-3xl text-neutral-900">{props.heading}</h2>} {props.heading && <h2 className="font-cal text-center text-3xl text-neutral-900">{props.heading}</h2>}
@@ -3,7 +3,7 @@ import { debounce, noop } from "lodash";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { RefCallback, useEffect, useMemo, useState } from "react"; import { RefCallback, useEffect, useMemo, useState } from "react";
import { getPremiumPlanMode, getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils"; import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
import { fetchUsername } from "@calcom/lib/fetchUsername"; import { fetchUsername } from "@calcom/lib/fetchUsername";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata"; import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -24,9 +24,7 @@ import {
} from "@calcom/ui"; } from "@calcom/ui";
export enum UsernameChangeStatusEnum { export enum UsernameChangeStatusEnum {
NORMAL = "NORMAL",
UPGRADE = "UPGRADE", UPGRADE = "UPGRADE",
DOWNGRADE = "DOWNGRADE",
} }
interface ICustomUsernameProps { interface ICustomUsernameProps {
@@ -60,20 +58,14 @@ interface ICustomUsernameProps {
const obtainNewUsernameChangeCondition = ({ const obtainNewUsernameChangeCondition = ({
userIsPremium, userIsPremium,
isNewUsernamePremium, isNewUsernamePremium,
stripeCustomer,
}: { }: {
userIsPremium: boolean; userIsPremium: boolean;
isNewUsernamePremium: boolean; isNewUsernamePremium: boolean;
stripeCustomer: RouterOutputs["viewer"]["stripeCustomer"] | undefined; stripeCustomer: RouterOutputs["viewer"]["stripeCustomer"] | undefined;
}) => { }) => {
if (!userIsPremium && isNewUsernamePremium && !stripeCustomer?.paidForPremium) { if (!userIsPremium && isNewUsernamePremium) {
return UsernameChangeStatusEnum.UPGRADE; return UsernameChangeStatusEnum.UPGRADE;
} }
if (userIsPremium && !isNewUsernamePremium && getPremiumPlanMode() === "subscription") {
return UsernameChangeStatusEnum.DOWNGRADE;
}
return UsernameChangeStatusEnum.NORMAL;
}; };
const PremiumTextfield = (props: ICustomUsernameProps) => { const PremiumTextfield = (props: ICustomUsernameProps) => {
@@ -106,7 +98,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
setIsInputUsernamePremium(data.premium); setIsInputUsernamePremium(data.premium);
setUsernameIsAvailable(data.available); setUsernameIsAvailable(data.available);
}, 150), }, 150),
[] [currentUsername]
); );
useEffect(() => { useEffect(() => {
@@ -137,7 +129,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
}); });
// when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for // when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for
const paymentRequired = !currentUsername && stripeCustomer?.isPremium && !stripeCustomer?.paidForPremium; const paymentRequired = !currentUsername && stripeCustomer?.isPremium;
const usernameChangeCondition = obtainNewUsernameChangeCondition({ const usernameChangeCondition = obtainNewUsernameChangeCondition({
userIsPremium: isCurrentUsernamePremium, userIsPremium: isCurrentUsernamePremium,
@@ -194,7 +186,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
}; };
const saveUsername = () => { const saveUsername = () => {
if (usernameChangeCondition === UsernameChangeStatusEnum.NORMAL) { if (usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE) {
updateUsername.mutate({ updateUsername.mutate({
username: inputUsernameValue, username: inputUsernameValue,
}); });
@@ -279,14 +271,6 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
{paymentMsg} {paymentMsg}
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>} {markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
{usernameIsAvailable && (
<p className={classNames("mt-1 text-xs text-gray-900")}>
{usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE && (
<>{t("premium_to_standard_username_description")}</>
)}
</p>
)}
<Dialog open={openDialogSaveUsername}> <Dialog open={openDialogSaveUsername}>
<DialogContent> <DialogContent>
<div className="flex flex-row"> <div className="flex flex-row">
@@ -295,13 +279,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
</div> </div>
<div className="mb-4 w-full px-4 pt-1"> <div className="mb-4 w-full px-4 pt-1">
<DialogHeader title={t("confirm_username_change_dialog_title")} /> <DialogHeader title={t("confirm_username_change_dialog_title")} />
{usernameChangeCondition && usernameChangeCondition !== UsernameChangeStatusEnum.NORMAL && ( {usernameChangeCondition && usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
<p className="-mt-4 mb-4 text-sm text-gray-800"> <p className="mb-4 text-sm text-gray-800">{t("change_username_standard_to_premium")}</p>
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE &&
t("change_username_standard_to_premium")}
{usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE &&
t("change_username_premium_to_standard")}
</p>
)} )}
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm"> <div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
@@ -323,8 +302,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<div className="mt-4 flex flex-row-reverse gap-x-2"> <div className="mt-4 flex flex-row-reverse gap-x-2">
{/* redirect to checkout */} {/* redirect to checkout */}
{(usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE || {usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE) && (
<Button <Button
type="button" type="button"
loading={updateUsername.isLoading} loading={updateUsername.isLoading}
@@ -336,7 +314,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
</Button> </Button>
)} )}
{/* Normal save */} {/* Normal save */}
{usernameChangeCondition === UsernameChangeStatusEnum.NORMAL && ( {usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE && (
<Button <Button
type="button" type="button"
loading={updateUsername.isLoading} loading={updateUsername.isLoading}
@@ -347,10 +325,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
{t("save")} {t("save")}
</Button> </Button>
)} )}
<DialogClose asChild> <DialogClose color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
<Button color="secondary" onClick={() => setOpenDialogSaveUsername(false)}> {t("cancel")}
{t("cancel")}
</Button>
</DialogClose> </DialogClose>
</div> </div>
</DialogContent> </DialogContent>
@@ -124,6 +124,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
<Input <Input
ref={usernameRef} ref={usernameRef}
name="username" name="username"
value={inputUsernameValue}
autoComplete="none" autoComplete="none"
autoCapitalize="none" autoCapitalize="none"
autoCorrect="none" autoCorrect="none"
@@ -133,7 +134,6 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0" ? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
: "" : ""
)} )}
defaultValue={currentUsername}
onChange={(event) => { onChange={(event) => {
event.preventDefault(); event.preventDefault();
setInputUsernameValue(event.target.value); setInputUsernameValue(event.target.value);
@@ -194,10 +194,8 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
{t("save")} {t("save")}
</Button> </Button>
<DialogClose asChild> <DialogClose color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
<Button color="secondary" onClick={() => setOpenDialogSaveUsername(false)}> {t("cancel")}
{t("cancel")}
</Button>
</DialogClose> </DialogClose>
</div> </div>
</DialogContent> </DialogContent>
@@ -1,6 +1,67 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { IS_SELF_HOSTED } from "@calcom/lib/constants"; import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import { User } from "@calcom/prisma/client";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { AppRouter } from "@calcom/trpc/server/routers/_app";
import { PremiumTextfield } from "./PremiumTextfield"; import { PremiumTextfield } from "./PremiumTextfield";
import { UsernameTextfield } from "./UsernameTextfield"; import { UsernameTextfield } from "./UsernameTextfield";
export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield; export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield;
interface UsernameAvailabilityFieldProps {
onSuccessMutation?: () => void;
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
user: Pick<
User,
| "username"
| "name"
| "email"
| "bio"
| "avatar"
| "timeZone"
| "weekStart"
| "hideBranding"
| "theme"
| "plan"
| "brandColor"
| "darkBrandColor"
| "timeFormat"
| "metadata"
>;
}
export const UsernameAvailabilityField = ({
onSuccessMutation,
onErrorMutation,
user,
}: UsernameAvailabilityFieldProps) => {
const [currentUsername, setCurrentUsername] = useState(user.username ?? "");
const formMethods = useForm({
defaultValues: {
username: currentUsername,
},
});
return (
<Controller
control={formMethods.control}
name="username"
render={({ field: { ref, onChange, value } }) => {
return (
<UsernameAvailability
currentUsername={currentUsername}
setCurrentUsername={setCurrentUsername}
inputUsernameValue={value}
usernameRef={ref}
setInputUsernameValue={onChange}
onSuccessMutation={onSuccessMutation}
onErrorMutation={onErrorMutation}
user={user}
/>
);
}}
/>
);
};
+2 -2
View File
@@ -1,6 +1,6 @@
import { DefaultSeoProps, NextSeoProps } from "next-seo"; import { DefaultSeoProps, NextSeoProps } from "next-seo";
import { SEO_IMG_DEFAULT, SEO_IMG_OGIMG } from "@calcom/lib/constants"; import { APP_NAME, SEO_IMG_DEFAULT, SEO_IMG_OGIMG } from "@calcom/lib/constants";
export type HeadSeoProps = { export type HeadSeoProps = {
title: string; title: string;
@@ -27,7 +27,7 @@ export const seoConfig: {
defaultNextSeo: DefaultSeoProps; defaultNextSeo: DefaultSeoProps;
} = { } = {
headSeo: { headSeo: {
siteName: "Cal.com", siteName: APP_NAME,
}, },
defaultNextSeo: { defaultNextSeo: {
twitter: { twitter: {
+1 -1
View File
@@ -2,7 +2,7 @@ import { useRouter } from "next/router";
export default function usePublicPage() { export default function usePublicPage() {
const router = useRouter(); const router = useRouter();
const isPublicPage = ["/[user]", "/success", "/cancel", "/reschedule"].find((route) => const isPublicPage = ["/[user]", "/booking", "/cancel", "/reschedule"].find((route) =>
router.pathname.startsWith(route) router.pathname.startsWith(route)
); );
return isPublicPage; return isPublicPage;
+18
View File
@@ -0,0 +1,18 @@
import { useRouter } from "next/router";
export default function useRouterQuery<T extends string>(name: T) {
const router = useRouter();
const existingQueryParams = router.asPath.split("?")[1];
const urlParams = new URLSearchParams(existingQueryParams);
const query = Object.fromEntries(urlParams);
const setQuery = (newValue: string | number | null | undefined) => {
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
};
return { [name]: query[name], setQuery } as {
[K in T]: string | undefined;
} & { setQuery: typeof setQuery };
}
+2 -4
View File
@@ -1,5 +1,3 @@
import { User } from "@prisma/client"; export function isBrandingHidden(hideBrandingSetting: boolean, belongsToActiveTeam: boolean) {
return belongsToActiveTeam && hideBrandingSetting;
export function isBrandingHidden<TUser extends Pick<User, "hideBranding" | "plan">>(user: TUser) {
return user.hideBranding && user.plan !== "FREE";
} }
+23
View File
@@ -7,6 +7,29 @@ export type EmbedProps = {
export default function withEmbedSsr(getServerSideProps: GetServerSideProps) { export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => { return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
const ssrResponse = await getServerSideProps(context); const ssrResponse = await getServerSideProps(context);
const embed = context.query.embed;
if ("redirect" in ssrResponse) {
// Use a dummy URL https://base as the fallback base URL so that URL parsing works for relative URLs as well.
const destinationUrlObj = new URL(ssrResponse.redirect.destination, "https://base");
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
const newDestinationUrl =
destinationUrlObj.pathname +
"/embed?" +
destinationUrlObj.searchParams.toString() +
"&embed=" +
embed;
return {
...ssrResponse,
redirect: {
...ssrResponse.redirect,
destination: newDestinationUrl,
},
};
}
if (!("props" in ssrResponse)) { if (!("props" in ssrResponse)) {
return ssrResponse; return ssrResponse;
} }
+15
View File
@@ -149,6 +149,21 @@ const nextConfig = {
source: "/router", source: "/router",
destination: "/apps/routing-forms/router", destination: "/apps/routing-forms/router",
}, },
{
source: "/success/:path*",
has: [
{
type: "query",
key: "uid",
value: "(?<uid>.*)",
},
],
destination: "/booking/:uid/:path*",
},
{
source: "/cancel/:path*",
destination: "/booking/:path*",
},
/* TODO: have these files being served from another deployment or CDN { /* TODO: have these files being served from another deployment or CDN {
source: "/embed/embed.js", source: "/embed/embed.js",
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?, destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
+4 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "2.3.1", "version": "2.3.6",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
@@ -38,7 +38,7 @@
"@calcom/tsconfig": "*", "@calcom/tsconfig": "*",
"@calcom/ui": "*", "@calcom/ui": "*",
"@daily-co/daily-js": "^0.26.0", "@daily-co/daily-js": "^0.26.0",
"@formkit/auto-animate": "^1.0.0-beta.1", "@formkit/auto-animate": "^1.0.0-beta.5",
"@glidejs/glide": "^3.5.2", "@glidejs/glide": "^3.5.2",
"@heroicons/react": "^1.0.6", "@heroicons/react": "^1.0.6",
"@hookform/error-message": "^2.0.0", "@hookform/error-message": "^2.0.0",
@@ -107,12 +107,14 @@
"react-live-chat-loader": "^2.7.3", "react-live-chat-loader": "^2.7.3",
"react-multi-email": "^0.5.3", "react-multi-email": "^0.5.3",
"react-phone-number-input": "^3.2.7", "react-phone-number-input": "^3.2.7",
"react-schemaorg": "^2.0.0",
"react-select": "^5.4.0", "react-select": "^5.4.0",
"react-timezone-select": "^1.3.2", "react-timezone-select": "^1.3.2",
"react-use-intercom": "1.5.1", "react-use-intercom": "1.5.1",
"react-virtualized-auto-sizer": "^1.0.6", "react-virtualized-auto-sizer": "^1.0.6",
"react-window": "^1.8.7", "react-window": "^1.8.7",
"rrule": "^2.7.1", "rrule": "^2.7.1",
"schema-dts": "^1.1.0",
"short-uuid": "^4.2.0", "short-uuid": "^4.2.0",
"stripe": "^9.16.0", "stripe": "^9.16.0",
"superjson": "1.9.1", "superjson": "1.9.1",
+9 -5
View File
@@ -1,9 +1,9 @@
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants"; import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { Icon } from "@calcom/ui"; import { Icon } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
@@ -38,7 +38,7 @@ export default function Custom404() {
setUrl(`${WEBSITE_URL}/signup?username=${username.replace("/", "")}`); setUrl(`${WEBSITE_URL}/signup?username=${username.replace("/", "")}`);
}, [username]); }, [username]);
const isSuccessPage = router.asPath.startsWith("/success"); const isSuccessPage = router.asPath.startsWith("/booking");
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage; const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
const isSignup = router.asPath.startsWith("/signup"); const isSignup = router.asPath.startsWith("/signup");
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com"; const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
@@ -64,7 +64,7 @@ export default function Custom404() {
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900"> <h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900">
{t("signup_requires")} {t("signup_requires")}
</h1> </h1>
<p className="mt-4">{t("signup_requires_description")}</p> <p className="mt-4">{t("signup_requires_description", { companyName: COMPANY_NAME })}</p>
</div> </div>
<div className="mt-12"> <div className="mt-12">
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500"> <h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
@@ -191,7 +191,11 @@ export default function Custom404() {
</span> </span>
) : isCalcom ? ( ) : isCalcom ? (
<a target="_blank" href={url} className="mt-2 inline-block text-lg" rel="noreferrer"> <a target="_blank" href={url} className="mt-2 inline-block text-lg" rel="noreferrer">
{t("the_username")} <strong className="text-blue-500">cal.com{username}</strong>{" "} {t("the_username")}{" "}
<strong className="text-blue-500">
{new URL(WEBSITE_URL).hostname}
{username}
</strong>{" "}
{t("is_still_available")} <span className="text-blue-500">{t("register_now")}</span>. {t("is_still_available")} <span className="text-blue-500">{t("register_now")}</span>.
</a> </a>
) : ( ) : (
+6 -3
View File
@@ -1,13 +1,16 @@
import Head from "next/head"; import Head from "next/head";
import { WEBSITE_URL } from "@calcom/lib/constants"; import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui"; import { Button } from "@calcom/ui";
export default function Error500() { export default function Error500() {
const { t } = useLocale();
return ( return (
<div className="flex h-screen"> <div className="flex h-screen">
<Head> <Head>
<title>Something unexpected occurred | Cal.com</title> <title>Something unexpected occurred | {APP_NAME}</title>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<div className="m-auto text-center"> <div className="m-auto text-center">
@@ -24,7 +27,7 @@ export default function Error500() {
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
away for you. away for you.
</p> </p>
<Button href={`${WEBSITE_URL}/support`}>Contact support</Button> <Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
<Button color="secondary" href="javascript:history.back()" className="ml-2"> <Button color="secondary" href="javascript:history.back()" className="ml-2">
Go back Go back
</Button> </Button>
+18 -3
View File
@@ -10,8 +10,9 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { User } from "@calcom/prisma/client"; import { User } from "@calcom/prisma/client";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr"; import { EmbedProps } from "@lib/withEmbedSsr";
@@ -110,6 +111,11 @@ async function getUserPageProps(context: GetStaticPropsContext) {
}, },
], ],
}, },
teams: {
include: {
team: true,
},
},
}, },
}); });
@@ -121,7 +127,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
"strikethrough", "strikethrough",
]); ]);
if (!user || !user.eventTypes) return { notFound: true }; if (!user || !user.eventTypes.length) return { notFound: true };
const [eventType]: (typeof user.eventTypes[number] & { const [eventType]: (typeof user.eventTypes[number] & {
users: Pick<User, "name" | "username" | "hideBranding" | "plan" | "timeZone">[]; users: Pick<User, "name" | "username" | "hideBranding" | "plan" | "timeZone">[];
@@ -150,6 +156,13 @@ async function getUserPageProps(context: GetStaticPropsContext) {
locations: privacyFilteredLocations(locations), locations: privacyFilteredLocations(locations),
descriptionAsSafeHTML: eventType.description ? md.render(eventType.description) : null, descriptionAsSafeHTML: eventType.description ? md.render(eventType.description) : null,
}); });
// Check if the user you are logging into has any active teams
const hasActiveTeam =
user.teams.filter((m) => {
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return false;
return true;
}).length > 0;
return { return {
props: { props: {
@@ -167,6 +180,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
away: user?.away, away: user?.away,
isDynamic: false, isDynamic: false,
trpcState: ssg.dehydrate(), trpcState: ssg.dehydrate(),
isBrandingHidden: isBrandingHidden(user.hideBranding, hasActiveTeam),
}, },
revalidate: 10, // seconds revalidate: 10, // seconds
}; };
@@ -224,7 +238,7 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : []; const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
const eventTypeObject = Object.assign({}, eventType, { const eventTypeObject = Object.assign({}, eventType, {
metadata: (eventType.metadata || {}) as JSONObject, metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
recurringEvent: parseRecurringEvent(eventType.recurringEvent), recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: privacyFilteredLocations(locations), locations: privacyFilteredLocations(locations),
users: users.map((user) => { users: users.map((user) => {
@@ -262,6 +276,7 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
isDynamic: true, isDynamic: true,
away: false, away: false,
trpcState: ssg.dehydrate(), trpcState: ssg.dehydrate(),
isBrandingHidden: false, // I think we should always show branding for dynamic groups - saves us checking every single user
}, },
revalidate: 10, // seconds revalidate: 10, // seconds
}; };
+3 -1
View File
@@ -12,6 +12,7 @@ import {
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { bookEventTypeSelect } from "@calcom/prisma"; import { bookEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull"; import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import getBooking, { GetBookingType } from "@lib/getBooking"; import getBooking, { GetBookingType } from "@lib/getBooking";
@@ -113,7 +114,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventType = { const eventType = {
...eventTypeRaw, ...eventTypeRaw,
metadata: (eventTypeRaw.metadata || {}) as JSONObject, metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent), recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
}; };
@@ -126,6 +127,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
periodStartDate: e.periodStartDate?.toString() ?? null, periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null, periodEndDate: e.periodEndDate?.toString() ?? null,
schedulingType: null, schedulingType: null,
customInputs: customInputSchema.array().parse(e.customInputs || []),
users: users.map((u) => ({ users: users.map((u) => ({
id: u.id, id: u.id,
name: u.name, name: u.name,
+3 -4
View File
@@ -3,10 +3,9 @@ import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps
type Props = Record<string, unknown> & DocumentProps; type Props = Record<string, unknown> & DocumentProps;
function toRunBeforeReactOnClient() { function toRunBeforeReactOnClient() {
const calEmbedMode = const calEmbedMode = typeof new URL(document.URL).searchParams.get("embed") === "string";
location.search.includes("embed=") || /* Iframe Name */
/* Iframe Name */ window.name.includes("cal-embed");
window.name.includes("cal-embed");
window.isEmbed = () => { window.isEmbed = () => {
// Once an embed mode always an embed mode // Once an embed mode always an embed mode
+22 -4
View File
@@ -13,13 +13,14 @@ import path from "path";
import checkLicense from "@calcom/features/ee/common/server/checkLicense"; import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider"; import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { ErrorCode, verifyPassword, isPasswordValid } from "@calcom/lib/auth"; import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto"; import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies"; import { defaultCookies } from "@calcom/lib/default-cookies";
import rateLimit from "@calcom/lib/rateLimit"; import rateLimit from "@calcom/lib/rateLimit";
import { serverConfig } from "@calcom/lib/serverConfig"; import { serverConfig } from "@calcom/lib/serverConfig";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import CalComAdapter from "@lib/auth/next-auth-custom-adapter"; import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
import { randomString } from "@lib/random"; import { randomString } from "@lib/random";
@@ -63,6 +64,11 @@ const providers: Provider[] = [
password: true, password: true,
twoFactorEnabled: true, twoFactorEnabled: true,
twoFactorSecret: true, twoFactorSecret: true,
teams: {
include: {
team: true,
},
},
}, },
}); });
@@ -116,6 +122,13 @@ const providers: Provider[] = [
intervalInMs: 60 * 1000, // 1 minute intervalInMs: 60 * 1000, // 1 minute
}); });
await limiter.check(10, user.email); // 10 requests per minute await limiter.check(10, user.email); // 10 requests per minute
// Check if the user you are logging into has any active teams
const hasActiveTeams =
user.teams.filter((m) => {
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return false;
return true;
}).length > 0;
// authentication success- but does it meet the minimum password requirements? // authentication success- but does it meet the minimum password requirements?
if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) { if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) {
@@ -125,6 +138,7 @@ const providers: Provider[] = [
email: user.email, email: user.email,
name: user.name, name: user.name,
role: "USER", role: "USER",
belongsToActiveTeam: hasActiveTeams,
}; };
} }
@@ -134,6 +148,7 @@ const providers: Provider[] = [
email: user.email, email: user.email,
name: user.name, name: user.name,
role: user.role, role: user.role,
belongsToActiveTeam: hasActiveTeams,
}; };
}, },
}), }),
@@ -204,9 +219,9 @@ if (true) {
}); });
const emailTemplate = Handlebars.compile(emailFile); const emailTemplate = Handlebars.compile(emailFile);
transporter.sendMail({ transporter.sendMail({
from: `${process.env.EMAIL_FROM}` || "Cal.com", from: `${process.env.EMAIL_FROM}` || APP_NAME,
to: identifier, to: identifier,
subject: "Your sign-in link for Cal.com", subject: "Your sign-in link for " + APP_NAME,
html: emailTemplate({ html: emailTemplate({
base_url: WEBAPP_URL, base_url: WEBAPP_URL,
signin_url: url, signin_url: url,
@@ -272,6 +287,7 @@ export default NextAuth({
email: user.email, email: user.email,
role: user.role, role: user.role,
impersonatedByUID: user?.impersonatedByUID, impersonatedByUID: user?.impersonatedByUID,
belongsToActiveTeam: user?.belongsToActiveTeam,
}; };
} }
@@ -307,6 +323,7 @@ export default NextAuth({
email: existingUser.email, email: existingUser.email,
role: existingUser.role, role: existingUser.role,
impersonatedByUID: token.impersonatedByUID as number, impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
}; };
} }
@@ -324,6 +341,7 @@ export default NextAuth({
username: token.username as string, username: token.username as string,
role: token.role as UserPermissionRole, role: token.role as UserPermissionRole,
impersonatedByUID: token.impersonatedByUID as number, impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
}, },
}; };
return calendsoSession; return calendsoSession;
-68
View File
@@ -1,68 +0,0 @@
import { MembershipRole } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import slugify from "@lib/slugify";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
if (!session?.user?.id) {
res.status(401).json({ message: "Not authenticated" });
return;
}
const ownerUser = await prisma.user.findFirst({
where: {
id: session.user.id,
},
select: {
id: true,
username: true,
createdDate: true,
name: true,
plan: true,
email: true,
},
});
if (req.method === "POST") {
const slug = slugify(req.body.name);
const nameCollisions = await prisma.team.count({
where: {
OR: [{ name: req.body.name }, { slug: slug }],
},
});
if (nameCollisions > 0) {
return res.status(409).json({ errorCode: "TeamNameCollision", message: "Team name already taken." });
}
const createTeam = await prisma.team.create({
data: {
name: req.body.name,
slug: slug,
},
});
await prisma.membership.create({
data: {
teamId: createTeam.id,
userId: session.user.id,
role: MembershipRole.OWNER,
accepted: true,
},
});
// Sync Services: Close.com
closeComUpsertTeamUser(createTeam, ownerUser, MembershipRole.OWNER);
return res.status(201).json({ message: "Team created" });
}
res.status(404).json({ message: "Team not found" });
}
+39 -5
View File
@@ -1,21 +1,55 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { InferGetStaticPropsType, NextPageContext } from "next"; import { InferGetStaticPropsType, NextPageContext } from "next";
import { ChangeEventHandler, useState } from "react";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry"; import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { classNames } from "@calcom/lib";
import { getSession } from "@calcom/lib/auth"; import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/client"; import type { AppCategories } from "@calcom/prisma/client";
import { AllApps, AppsLayout, AppStoreCategories, TrendingAppsSlider } from "@calcom/ui"; import { AllApps, AppsLayout, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
import { ssgInit } from "@server/lib/ssg"; import { ssgInit } from "@server/lib/ssg";
function AppsSearch({
onChange,
className,
}: {
onChange: ChangeEventHandler<HTMLInputElement>;
className?: string;
}) {
return (
<TextField
className="!border-gray-100 bg-gray-100 !pl-0 focus:!ring-offset-0"
addOnLeading={<Icon.FiSearch className="h-4 w-4 text-gray-500" />}
addOnClassname="!border-gray-100"
containerClassName={classNames("focus:!ring-offset-0", className)}
type="search"
autoComplete="false"
onChange={onChange}
/>
);
}
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getServerSideProps>) { export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getServerSideProps>) {
const { t } = useLocale(); const { t } = useLocale();
const [searchText, setSearchText] = useState<string | undefined>(undefined);
return ( return (
<AppsLayout isPublic heading={t("app_store")} subtitle={t("app_store_description")}> <AppsLayout
<AppStoreCategories categories={categories} /> isPublic
<TrendingAppsSlider items={appStore} /> heading={t("app_store")}
<AllApps apps={appStore} /> subtitle={t("app_store_description")}
actions={(className) => (
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
)}>
{!searchText && (
<>
<AppStoreCategories categories={categories} />
<TrendingAppsSlider items={appStore} />
</>
)}
<AllApps apps={appStore} searchText={searchText} />
</AppsLayout> </AppsLayout>
); );
} }
+1 -1
View File
@@ -4,6 +4,7 @@ import z from "zod";
import { AppSettings } from "@calcom/app-store/_components/AppSettings"; import { AppSettings } from "@calcom/app-store/_components/AppSettings";
import { InstallAppButton } from "@calcom/app-store/components"; import { InstallAppButton } from "@calcom/app-store/components";
import { InstalledAppVariants } from "@calcom/app-store/utils"; import { InstalledAppVariants } from "@calcom/app-store/utils";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { App } from "@calcom/types/App"; import { App } from "@calcom/types/App";
@@ -11,7 +12,6 @@ import { AppGetServerSidePropsContext } from "@calcom/types/AppGetServerSideProp
import { import {
Alert, Alert,
Button, Button,
DisconnectIntegration,
EmptyScreen, EmptyScreen,
Icon, Icon,
InstalledAppsLayout, InstalledAppsLayout,
+2 -2
View File
@@ -3,7 +3,7 @@ import { signIn } from "next-auth/react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect } from "react";
import { getPremiumPlanPrice } from "@calcom/app-store/stripepayment/lib/utils"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
import stripe from "@calcom/features/ee/payments/server/stripe"; import stripe from "@calcom/features/ee/payments/server/stripe";
import { import {
hostedCal, hostedCal,
@@ -161,7 +161,7 @@ const getStripePremiumUsernameUrl = async ({
customer: customer.id, customer: customer.id,
line_items: [ line_items: [
{ {
price: getPremiumPlanPrice(), price: getPremiumMonthlyPlanPriceId(),
quantity: 1, quantity: 1,
}, },
], ],
+6 -2
View File
@@ -29,6 +29,8 @@ import { HttpError } from "@lib/core/http/error";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader"; import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import EditableHeading from "@components/ui/EditableHeading"; import EditableHeading from "@components/ui/EditableHeading";
import { ssgInit } from "@server/lib/ssg";
const querySchema = z.object({ const querySchema = z.object({
schedule: stringOrNumber, schedule: stringOrNumber,
}); });
@@ -91,7 +93,7 @@ export default function Availability({ schedule }: { schedule: number }) {
return ( return (
<Shell <Shell
backPath="/availability" backPath="/availability"
title={data?.schedule.name && data.schedule.name + " | " + t("availability")} title={data?.schedule.name ? data.schedule.name + " | " + t("availability") : t("availability")}
heading={ heading={
<Controller <Controller
control={form.control} control={form.control}
@@ -209,14 +211,16 @@ export default function Availability({ schedule }: { schedule: number }) {
); );
} }
export const getStaticProps: GetStaticProps = (ctx) => { export const getStaticProps: GetStaticProps = async (ctx) => {
const params = querySchema.safeParse(ctx.params); const params = querySchema.safeParse(ctx.params);
const ssg = await ssgInit(ctx);
if (!params.success) return { notFound: true }; if (!params.success) return { notFound: true };
return { return {
props: { props: {
schedule: params.data.schedule, schedule: params.data.schedule,
trpcState: ssg.dehydrate(),
}, },
revalidate: 10, // seconds revalidate: 10, // seconds
}; };
+32
View File
@@ -1,4 +1,5 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { GetServerSidePropsContext } from "next";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules"; import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -10,6 +11,8 @@ import { HttpError } from "@lib/core/http/error";
import SkeletonLoader from "@components/availability/SkeletonLoader"; import SkeletonLoader from "@components/availability/SkeletonLoader";
import { ssrInit } from "@server/lib/ssr";
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) { export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
const { t } = useLocale(); const { t } = useLocale();
const utils = trpc.useContext(); const utils = trpc.useContext();
@@ -45,6 +48,24 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
}, },
}); });
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
onSuccess: async ({ schedule }) => {
await utils.viewer.availability.list.invalidate();
showToast(
t("availability_updated_successfully", {
scheduleName: schedule.name,
}),
"success"
);
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
}
},
});
// Adds smooth delete button - item fades and old item slides into place // Adds smooth delete button - item fades and old item slides into place
const [animationParentRef] = useAutoAnimate<HTMLUListElement>(); const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
@@ -71,6 +92,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
}} }}
key={schedule.id} key={schedule.id}
schedule={schedule} schedule={schedule}
updateDefault={updateMutation.mutate}
deleteFunction={deleteMutation.mutate} deleteFunction={deleteMutation.mutate}
/> />
))} ))}
@@ -93,3 +115,13 @@ export default function AvailabilityPage() {
</div> </div>
); );
} }
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
@@ -23,6 +23,7 @@ import {
} from "@calcom/embed-core/embed-iframe"; } from "@calcom/embed-core/embed-iframe";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import CustomBranding from "@calcom/lib/CustomBranding"; import CustomBranding from "@calcom/lib/CustomBranding";
import { APP_NAME } from "@calcom/lib/constants";
import { formatTime } from "@calcom/lib/date-fns"; import { formatTime } from "@calcom/lib/date-fns";
import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -33,13 +34,14 @@ import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/t
import { localStorage } from "@calcom/lib/webstorage"; import { localStorage } from "@calcom/lib/webstorage";
import prisma, { baseUserSelect } from "@calcom/prisma"; import prisma, { baseUserSelect } from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client"; import { Prisma } from "@calcom/prisma/client";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Button, EmailInput, Icon } from "@calcom/ui"; import { Button, EmailInput, Icon } from "@calcom/ui";
import { timeZone } from "@lib/clock"; import { timeZone } from "@lib/clock";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import CancelBooking from "@components/booking/CancelBooking"; import CancelBooking from "@components/booking/CancelBooking";
import EventReservationSchema from "@components/schemas/EventReservationSchema";
import { HeadSeo } from "@components/seo/head-seo"; import { HeadSeo } from "@components/seo/head-seo";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
@@ -147,6 +149,7 @@ const querySchema = z.object({
uid: z.string(), uid: z.string(),
allRemainingBookings: stringToBoolean, allRemainingBookings: stringToBoolean,
cancel: stringToBoolean, cancel: stringToBoolean,
changes: stringToBoolean,
reschedule: stringToBoolean, reschedule: stringToBoolean,
isSuccessBookingPage: z.string().optional(), isSuccessBookingPage: z.string().optional(),
}); });
@@ -159,26 +162,33 @@ export default function Success(props: SuccessProps) {
allRemainingBookings, allRemainingBookings,
isSuccessBookingPage, isSuccessBookingPage,
cancel: isCancellationMode, cancel: isCancellationMode,
changes,
} = querySchema.parse(router.query); } = querySchema.parse(router.query);
if (isCancellationMode && typeof window !== "undefined") { if ((isCancellationMode || changes) && typeof window !== "undefined") {
window.scrollTo(0, document.body.scrollHeight); window.scrollTo(0, document.body.scrollHeight);
} }
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(props.bookingInfo.location) const location: ReturnType<typeof getEventLocationValue> = Array.isArray(props.bookingInfo.location)
? props.bookingInfo.location[0] || "" ? props.bookingInfo.location[0]
: props.bookingInfo.location || ""; : // If there is no location set then we default to Cal Video
"integrations:daily";
if (!location) { if (!location) {
// Can't use logger.error because it throws error on client. stdout isn't available to it. // Can't use logger.error because it throws error on client. stdout isn't available to it.
console.error(`No location found `); console.error(`No location found `);
} }
const name = props.bookingInfo?.user?.name;
const email = props.bookingInfo?.user?.email; const email = props.bookingInfo?.user?.email;
const status = props.bookingInfo?.status; const status = props.bookingInfo?.status;
const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED; const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED;
const cancellationReason = props.bookingInfo.cancellationReason; const cancellationReason = props.bookingInfo.cancellationReason;
const attendeeName =
typeof props?.bookingInfo?.attendees?.[0]?.name === "string"
? props?.bookingInfo?.attendees?.[0]?.name
: "Nameless";
const [is24h, setIs24h] = useState(isBrowserLocale24h()); const [is24h, setIs24h] = useState(isBrowserLocale24h());
const { data: session } = useSession(); const { data: session } = useSession();
@@ -189,6 +199,7 @@ export default function Success(props: SuccessProps) {
const isEmbed = useIsEmbed(); const isEmbed = useIsEmbed();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left"; const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed; const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
function setIsCancellationMode(value: boolean) { function setIsCancellationMode(value: boolean) {
if (value) router.query.cancel = "true"; if (value) router.query.cancel = "true";
@@ -199,8 +210,6 @@ export default function Success(props: SuccessProps) {
}); });
} }
const attendeeName = typeof name === "string" ? name : "Nameless";
const eventNameObject = { const eventNameObject = {
attendeeName, attendeeName,
eventType: props.eventType.title, eventType: props.eventType.title,
@@ -220,7 +229,7 @@ export default function Success(props: SuccessProps) {
useEffect(() => { useEffect(() => {
if (top !== window) { if (top !== window) {
//page_view will be collected automatically by _middleware.ts //page_view will be collected automatically by _middleware.ts
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/success")); telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/booking"));
} }
}, [telemetry]); }, [telemetry]);
@@ -231,7 +240,7 @@ export default function Success(props: SuccessProps) {
sdkActionManager.fire("bookingSuccessful", { sdkActionManager.fire("bookingSuccessful", {
eventType, eventType,
date: date.toString(), date: date.toString(),
duration: eventType.length, duration: calculatedDuration,
organizer: { organizer: {
name: users[0].name || "Nameless", name: users[0].name || "Nameless",
email: users[0].email || "Email-less", email: users[0].email || "Email-less",
@@ -245,6 +254,12 @@ export default function Success(props: SuccessProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventType, needsConfirmation]); }, [eventType, needsConfirmation]);
useEffect(() => {
setCalculatedDuration(
dayjs(props.bookingInfo.endTime).diff(dayjs(props.bookingInfo.startTime), "minutes")
);
}, []);
function eventLink(): string { function eventLink(): string {
const optional: { location?: string } = {}; const optional: { location?: string } = {};
if (location) { if (location) {
@@ -263,7 +278,9 @@ export default function Success(props: SuccessProps) {
title: eventName, title: eventName,
description: props.eventType.description ? props.eventType.description : undefined, description: props.eventType.description ? props.eventType.description : undefined,
/** formatted to required type of description ^ */ /** formatted to required type of description ^ */
duration: { minutes: props.eventType.length }, duration: {
minutes: calculatedDuration,
},
...optional, ...optional,
}); });
@@ -305,6 +322,19 @@ export default function Success(props: SuccessProps) {
return ( return (
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page"> <div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
{!isEmbed && (
<EventReservationSchema
reservationId={bookingInfo.uid}
eventName={eventName}
startTime={bookingInfo.startTime}
endTime={bookingInfo.endTime}
organizer={bookingInfo.user}
attendees={bookingInfo.attendees}
location={locationToDisplay}
description={bookingInfo.description}
status={status}
/>
)}
{userIsOwner && !isEmbed && ( {userIsOwner && !isEmbed && (
<div className="mt-2 ml-4 -mb-4"> <div className="mt-2 ml-4 -mb-4">
<Link href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}> <Link href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}>
@@ -361,7 +391,7 @@ export default function Success(props: SuccessProps) {
<div className="mt-6 mb-8 text-center last:mb-0"> <div className="mt-6 mb-8 text-center last:mb-0">
<h3 <h3
className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white" className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white"
data-testid="success-headline" data-testid={isCancelled ? "cancelled-headline" : ""}
id="modal-headline"> id="modal-headline">
{needsConfirmation && !isCancelled {needsConfirmation && !isCancelled
? props.recurringBookings ? props.recurringBookings
@@ -389,6 +419,7 @@ export default function Success(props: SuccessProps) {
<div className="col-span-2 mb-6 last:mb-0"> <div className="col-span-2 mb-6 last:mb-0">
<RecurringBookings <RecurringBookings
eventType={props.eventType} eventType={props.eventType}
duration={calculatedDuration}
recurringBookings={props.recurringBookings} recurringBookings={props.recurringBookings}
allRemainingBookings={allRemainingBookings} allRemainingBookings={allRemainingBookings}
date={date} date={date}
@@ -399,7 +430,7 @@ export default function Success(props: SuccessProps) {
{(bookingInfo?.user || bookingInfo?.attendees) && ( {(bookingInfo?.user || bookingInfo?.attendees) && (
<> <>
<div className="font-medium">{t("who")}</div> <div className="font-medium">{t("who")}</div>
<div className="col-span-2 mb-6 last:mb-0"> <div className="col-span-2 last:mb-0">
<> <>
{bookingInfo?.user && ( {bookingInfo?.user && (
<div className="mb-3"> <div className="mb-3">
@@ -441,13 +472,40 @@ export default function Success(props: SuccessProps) {
)} )}
{customInputs && {customInputs &&
Object.keys(customInputs).map((key) => { Object.keys(customInputs).map((key) => {
// This breaks if you have two label that are the same.
// TODO: Fix this in another PR
const customInput = customInputs[key as keyof typeof customInputs]; const customInput = customInputs[key as keyof typeof customInputs];
const eventTypeCustomFound = eventType.customInputs?.find((ci) => ci.label === key);
return ( return (
<> <>
{customInput !== "" && ( {eventTypeCustomFound?.type === "RADIO" && (
<> <>
<div className="mt-2 pr-3 font-medium">{key}</div> <div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
<div className="col-span-2 mt-2 mb-2"> {eventTypeCustomFound.label}
</div>
<div className="col-span-3 mt-1 mb-2">
{eventTypeCustomFound.options &&
eventTypeCustomFound.options.map((option) => {
const selected = option.label == customInput;
return (
<div
key={option.label}
className={classNames(
"flex space-x-1",
!selected && "text-gray-500"
)}>
<p>{option.label}</p>
<span>{option.label === customInput && "✅"}</span>
</div>
);
})}
</div>
</>
)}
{eventTypeCustomFound?.type !== "RADIO" && customInput !== "" && (
<>
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">{key}</div>
<div className="col-span-3 mt-2 mb-2">
{typeof customInput === "boolean" ? ( {typeof customInput === "boolean" ? (
<p>{customInput ? "true" : "false"}</p> <p>{customInput ? "true" : "false"}</p>
) : ( ) : (
@@ -469,7 +527,7 @@ export default function Success(props: SuccessProps) {
)} )}
</div> </div>
</div> </div>
{!needsConfirmation && {(!needsConfirmation || !userIsOwner) &&
!isCancelled && !isCancelled &&
(!isCancellationMode ? ( (!isCancellationMode ? (
<> <>
@@ -513,119 +571,125 @@ export default function Success(props: SuccessProps) {
/> />
</> </>
))} ))}
{userIsOwner && !needsConfirmation && !isCancellationMode && !isCancelled && ( {userIsOwner &&
<> !needsConfirmation &&
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" /> !isCancellationMode &&
<div className="text-bookingdark align-center flex flex-row justify-center pt-8"> !isCancelled &&
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50"> calculatedDuration && (
{t("add_to_calendar")} <>
</span> <hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
<div className="justify-left mt-1 flex text-left sm:mt-0"> <div className="text-bookingdark align-center flex flex-row justify-center pt-8">
<Link <span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
href={ {t("add_to_calendar")}
`https://calendar.google.com/calendar/r/eventedit?dates=${date </span>
.utc() <div className="justify-left mt-1 flex text-left sm:mt-0">
.format("YYYYMMDDTHHmmss[Z]")}/${date <Link
.add(props.eventType.length, "minute") href={
.utc() `https://calendar.google.com/calendar/r/eventedit?dates=${date
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${ .utc()
props.eventType.description .format("YYYYMMDDTHHmmss[Z]")}/${date
}` + .add(calculatedDuration, "minute")
(typeof location === "string" .utc()
? "&location=" + encodeURIComponent(location) .format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
: "") + props.eventType.description
(props.eventType.recurringEvent }` +
? "&recur=" + (typeof location === "string"
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString()) ? "&location=" + encodeURIComponent(location)
: "") : "") +
}> (props.eventType.recurringEvent
<a className="mr-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"> ? "&recur=" +
<svg encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
className="-mt-1.5 inline-block h-4 w-4" : "")
fill="currentColor" }>
xmlns="http://www.w3.org/2000/svg" <a className="mr-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white">
viewBox="0 0 24 24"> <svg
<title>Google</title> className="-mt-1.5 inline-block h-4 w-4"
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" /> fill="currentColor"
</svg> xmlns="http://www.w3.org/2000/svg"
</a> viewBox="0 0 24 24">
</Link> <title>Google</title>
<Link <path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
href={ </svg>
encodeURI( </a>
"https://outlook.live.com/calendar/0/deeplink/compose?body=" + </Link>
props.eventType.description + <Link
"&enddt=" + href={
date.add(props.eventType.length, "minute").utc().format() + encodeURI(
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" + "https://outlook.live.com/calendar/0/deeplink/compose?body=" +
date.utc().format() + props.eventType.description +
"&subject=" + "&enddt=" +
eventName date.add(calculatedDuration, "minute").utc().format() +
) + (location ? "&location=" + location : "") "&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
}> date.utc().format() +
<a "&subject=" +
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white" eventName
target="_blank"> ) + (location ? "&location=" + location : "")
<svg }>
className="mr-1 -mt-1.5 inline-block h-4 w-4" <a
fill="currentColor" className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
xmlns="http://www.w3.org/2000/svg" target="_blank">
viewBox="0 0 24 24"> <svg
<title>Microsoft Outlook</title> className="mr-1 -mt-1.5 inline-block h-4 w-4"
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" /> fill="currentColor"
</svg> xmlns="http://www.w3.org/2000/svg"
</a> viewBox="0 0 24 24">
</Link> <title>Microsoft Outlook</title>
<Link <path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
href={ </svg>
encodeURI( </a>
"https://outlook.office.com/calendar/0/deeplink/compose?body=" + </Link>
props.eventType.description + <Link
"&enddt=" + href={
date.add(props.eventType.length, "minute").utc().format() + encodeURI(
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" + "https://outlook.office.com/calendar/0/deeplink/compose?body=" +
date.utc().format() + props.eventType.description +
"&subject=" + "&enddt=" +
eventName date.add(calculatedDuration, "minute").utc().format() +
) + (location ? "&location=" + location : "") "&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
}> date.utc().format() +
<a "&subject=" +
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white" eventName
target="_blank"> ) + (location ? "&location=" + location : "")
<svg }>
className="mr-1 -mt-1.5 inline-block h-4 w-4" <a
fill="currentColor" className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
xmlns="http://www.w3.org/2000/svg" target="_blank">
viewBox="0 0 24 24"> <svg
<title>Microsoft Office</title> className="mr-1 -mt-1.5 inline-block h-4 w-4"
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" /> fill="currentColor"
</svg> xmlns="http://www.w3.org/2000/svg"
</a> viewBox="0 0 24 24">
</Link> <title>Microsoft Office</title>
<Link href={"data:text/calendar," + eventLink()}> <path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
<a </svg>
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white" </a>
download={props.eventType.title + ".ics"}> </Link>
<svg <Link href={"data:text/calendar," + eventLink()}>
version="1.1" <a
fill="currentColor" className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
xmlns="http://www.w3.org/2000/svg" download={props.eventType.title + ".ics"}>
viewBox="0 0 1000 1000" <svg
className="mr-1 -mt-1.5 inline-block h-4 w-4"> version="1.1"
<title>{t("other")}</title> fill="currentColor"
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" /> xmlns="http://www.w3.org/2000/svg"
</svg> viewBox="0 0 1000 1000"
</a> className="mr-1 -mt-1.5 inline-block h-4 w-4">
</Link> <title>{t("other")}</title>
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
</svg>
</a>
</Link>
</div>
</div> </div>
</div> </>
</> )}
)}
{session === null && !(userIsOwner || props.hideBranding) && ( {session === null && !(userIsOwner || props.hideBranding) && (
<> <>
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" /> <hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
<div className="border-bookinglightest text-booking-lighter dark:border-darkgray-300 pt-8 text-center text-xs dark:text-white"> <div className="border-bookinglightest text-booking-lighter dark:border-darkgray-300 pt-8 text-center text-xs dark:text-white">
<a href="https://cal.com/signup">{t("create_booking_link_with_calcom")}</a> <a href="https://cal.com/signup">
{t("create_booking_link_with_calcom", { appName: APP_NAME })}
</a>
<form <form
onSubmit={(e) => { onSubmit={(e) => {
@@ -669,6 +733,7 @@ type RecurringBookingsProps = {
eventType: SuccessProps["eventType"]; eventType: SuccessProps["eventType"];
recurringBookings: SuccessProps["recurringBookings"]; recurringBookings: SuccessProps["recurringBookings"];
date: dayjs.Dayjs; date: dayjs.Dayjs;
duration: number | undefined;
is24h: boolean; is24h: boolean;
allRemainingBookings: boolean; allRemainingBookings: boolean;
isCancelled: boolean; isCancelled: boolean;
@@ -677,6 +742,7 @@ type RecurringBookingsProps = {
export function RecurringBookings({ export function RecurringBookings({
eventType, eventType,
recurringBookings, recurringBookings,
duration,
date, date,
allRemainingBookings, allRemainingBookings,
is24h, is24h,
@@ -689,6 +755,8 @@ export function RecurringBookings({
? recurringBookings.sort((a: ConfigType, b: ConfigType) => (dayjs(a).isAfter(dayjs(b)) ? 1 : -1)) ? recurringBookings.sort((a: ConfigType, b: ConfigType) => (dayjs(a).isAfter(dayjs(b)) ? 1 : -1))
: null; : null;
if (!duration) return null;
if (recurringBookingsSorted && allRemainingBookings) { if (recurringBookingsSorted && allRemainingBookings) {
return ( return (
<> <>
@@ -707,7 +775,7 @@ export function RecurringBookings({
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")} {dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
<br /> <br />
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "} {formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
{formatTime(dayjs(dateStr).add(eventType.length, "m"), is24h ? 24 : 12, timeZone())}{" "} {formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
<span className="text-bookinglight">({timeZone()})</span> <span className="text-bookinglight">({timeZone()})</span>
</div> </div>
))} ))}
@@ -725,7 +793,7 @@ export function RecurringBookings({
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")} {dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
<br /> <br />
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "} {formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
{formatTime(dayjs(dateStr).add(eventType.length, "m"), is24h ? 24 : 12, timeZone())}{" "} {formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
<span className="text-bookinglight">({timeZone()})</span> <span className="text-bookinglight">({timeZone()})</span>
</div> </div>
))} ))}
@@ -741,7 +809,7 @@ export function RecurringBookings({
{dayjs.tz(date, timeZone()).format("MMMM DD, YYYY")} {dayjs.tz(date, timeZone()).format("MMMM DD, YYYY")}
<br /> <br />
{formatTime(date, is24h ? 24 : 12, timeZone())} -{" "} {formatTime(date, is24h ? 24 : 12, timeZone())} -{" "}
{formatTime(dayjs(date).add(eventType.length, "m"), is24h ? 24 : 12, timeZone())}{" "} {formatTime(dayjs(date).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
<span className="text-bookinglight">({timeZone()})</span> <span className="text-bookinglight">({timeZone()})</span>
</div> </div>
); );
@@ -762,6 +830,7 @@ const getEventTypesFromDB = async (id: number) => {
requiresConfirmation: true, requiresConfirmation: true,
userId: true, userId: true,
successRedirectUrl: true, successRedirectUrl: true,
customInputs: true,
locations: true, locations: true,
price: true, price: true,
currency: true, currency: true,
@@ -828,22 +897,25 @@ const handleSeatsEventTypeOnBooking = (
seatsShowAttendees: boolean | null; seatsShowAttendees: boolean | null;
[x: string | number | symbol]: unknown; [x: string | number | symbol]: unknown;
}, },
booking: Partial< bookingInfo: Partial<
Prisma.BookingGetPayload<{ include: { attendees: { select: { name: true; email: true } } } }> Prisma.BookingGetPayload<{ include: { attendees: { select: { name: true; email: true } } } }>
>, >,
email: string email: string
) => { ) => {
if (eventType?.seatsPerTimeSlot !== null) { if (eventType?.seatsPerTimeSlot !== null) {
// @TODO: right now bookings with seats doesn't save every description that its entered by every user // @TODO: right now bookings with seats doesn't save every description that its entered by every user
delete booking.description; delete bookingInfo.description;
} else { } else {
return; return;
} }
if (!eventType.seatsShowAttendees) { if (!eventType.seatsShowAttendees) {
const attendee = booking?.attendees?.find((a) => a.email === email); const attendee = bookingInfo?.attendees?.find((a) => {
booking["attendees"] = attendee ? [attendee] : []; return a.email === email;
});
bookingInfo["attendees"] = attendee ? [attendee] : [];
} }
return; return bookingInfo;
}; };
export async function getServerSideProps(context: GetServerSidePropsContext) { export async function getServerSideProps(context: GetServerSidePropsContext) {
@@ -865,6 +937,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
smsReminderNumber: true, smsReminderNumber: true,
recurringEventId: true, recurringEventId: true,
startTime: true, startTime: true,
endTime: true,
location: true, location: true,
status: true, status: true,
cancellationReason: true, cancellationReason: true,
@@ -901,6 +974,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
// @NOTE: had to do this because Server side cant return [Object objects] // @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse // probably fixable with json.stringify -> json.parse
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date; bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
const eventTypeRaw = !bookingInfo.eventTypeId const eventTypeRaw = !bookingInfo.eventTypeId
? getDefaultEvent(eventTypeSlug || "") ? getDefaultEvent(eventTypeSlug || "")
@@ -920,7 +994,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
select: baseUserSelect, select: baseUserSelect,
}); });
if (user) { if (user) {
eventTypeRaw.users.push(user); eventTypeRaw.users.push({
...user,
avatar: "",
allowDynamicBooking: true,
});
} }
} }
@@ -936,6 +1014,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null, periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata), metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent), recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
}; };
const profile = { const profile = {
@@ -947,7 +1026,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
slug: eventType.team?.slug || eventType.users[0]?.username || null, slug: eventType.team?.slug || eventType.users[0]?.username || null,
}; };
if (bookingInfo !== null && email) { if (bookingInfo !== null && email && eventType.seatsPerTimeSlot) {
handleSeatsEventTypeOnBooking(eventType, bookingInfo, email); handleSeatsEventTypeOnBooking(eventType, bookingInfo, email);
} }
+77 -34
View File
@@ -14,9 +14,18 @@ import { useInViewObserver } from "@lib/hooks/useInViewObserver";
import BookingListItem from "@components/booking/BookingListItem"; import BookingListItem from "@components/booking/BookingListItem";
import SkeletonLoader from "@components/booking/SkeletonLoader"; import SkeletonLoader from "@components/booking/SkeletonLoader";
import { ssgInit } from "@server/lib/ssg";
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"]; type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0]; type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
type RecurringInfo = {
recurringEventId: string | null;
count: number;
firstDate: Date | null;
bookings: { [key: string]: Date[] };
};
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const; const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
const descriptionByStatus: Record<BookingListingStatus, string> = { const descriptionByStatus: Record<BookingListingStatus, string> = {
@@ -73,6 +82,18 @@ export default function Bookings() {
return true; return true;
}; };
let recurringInfoToday: RecurringInfo | undefined;
const bookingsToday =
query.data?.pages.map((page) =>
page.bookings.filter((booking: BookingOutput) => {
recurringInfoToday = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return new Date(booking.startTime).toDateString() === new Date().toDateString();
})
)[0] || [];
const [animationParentRef] = useAutoAnimate<HTMLDivElement>(); const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
return ( return (
@@ -83,45 +104,65 @@ export default function Bookings() {
)} )}
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />} {(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
{query.status === "success" && !isEmpty && ( {query.status === "success" && !isEmpty && (
<div className="pt-2 xl:pt-0"> <>
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} /> {!!bookingsToday.length && status === "upcoming" && (
{/* TODO: add today only for the current day <div className="mb-6 pt-2 xl:pt-0">
<p className="pb-3 text-xs font-medium leading-4 text-gray-500 uppercase">{t("today")}</p> <WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
*/} <p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("today")}</p>
<div className="overflow-hidden rounded-md border border-gray-200">
<div className="overflow-hidden rounded-md border border-gray-200"> <table className="w-full max-w-full table-fixed">
<table className="w-full max-w-full table-fixed"> <tbody className="divide-y divide-gray-200 bg-white">
<tbody className="divide-y divide-gray-200 bg-white" data-testid="bookings"> <Fragment>
{query.data.pages.map((page, index) => ( {bookingsToday.map((booking: BookingOutput) => (
<Fragment key={index}>
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
const recurringInfo = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
<BookingListItem <BookingListItem
key={booking.id} key={booking.id}
listingStatus={status} listingStatus={status}
recurringInfo={recurringInfo} recurringInfo={recurringInfoToday}
{...booking} {...booking}
/> />
); ))}
})} </Fragment>
</Fragment> </tbody>
))} </table>
</tbody> </div>
</table> </div>
)}
<div className="pt-2 xl:pt-0">
<p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("all")}</p>
<div className="overflow-hidden rounded-md border border-gray-200">
<table className="w-full max-w-full table-fixed">
<tbody className="divide-y divide-gray-200 bg-white" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
const recurringInfo = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
<BookingListItem
key={booking.id}
listingStatus={status}
recurringInfo={recurringInfo}
{...booking}
/>
);
})}
</Fragment>
))}
</tbody>
</table>
</div>
<div className="p-4 text-center" ref={buttonInView.ref}>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div> </div>
<div className="p-4 text-center" ref={buttonInView.ref}> </>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
)} )}
{query.status === "success" && isEmpty && ( {query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:mx-6 xl:pt-0"> <div className="flex items-center justify-center pt-2 xl:mx-6 xl:pt-0">
@@ -140,14 +181,16 @@ export default function Bookings() {
); );
} }
export const getStaticProps: GetStaticProps = (ctx) => { export const getStaticProps: GetStaticProps = async (ctx) => {
const params = querySchema.safeParse(ctx.params); const params = querySchema.safeParse(ctx.params);
const ssg = await ssgInit(ctx);
if (!params.success) return { notFound: true }; if (!params.success) return { notFound: true };
return { return {
props: { props: {
status: params.data.status, status: params.data.status,
trpcState: ssg.dehydrate(),
}, },
}; };
}; };
-24
View File
@@ -1,24 +0,0 @@
import { GetServerSidePropsContext } from "next";
import z from "zod";
const querySchema = z.object({
uid: z.string(),
allRemainingBookings: z
.string()
.optional()
.transform((val) => (val ? JSON.parse(val) : false)),
});
export default function Type() {
return <></>;
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { allRemainingBookings, uid } = querySchema.parse(context.query);
return {
redirect: {
permanent: false,
destination: `/success?uid=${uid}&allRemainingBookings=${allRemainingBookings}&cancel=true`,
},
};
};
+1
View File
@@ -166,6 +166,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
previousPage: context.req.headers.referer ?? null, previousPage: context.req.headers.referer ?? null,
booking, booking,
users: [user.username], users: [user.username],
isBrandingHidden: user.hideBranding,
}, },
}; };
}; };
+3 -1
View File
@@ -4,6 +4,7 @@ import { JSONObject } from "superjson/dist/types";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { bookEventTypeSelect } from "@calcom/prisma/selects"; import { bookEventTypeSelect } from "@calcom/prisma/selects";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull"; import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -69,7 +70,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!eventTypeRaw) return { notFound: true }; if (!eventTypeRaw) return { notFound: true };
const eventType = { const eventType = {
...eventTypeRaw, ...eventTypeRaw,
metadata: (eventTypeRaw.metadata || {}) as JSONObject, metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent), recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
}; };
@@ -78,6 +79,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
...e, ...e,
periodStartDate: e.periodStartDate?.toString() ?? null, periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null, periodEndDate: e.periodEndDate?.toString() ?? null,
customInputs: customInputSchema.array().parse(e.customInputs || []),
schedulingType: null, schedulingType: null,
users: users.map((u) => ({ users: users.map((u) => ({
id: u.id, id: u.id,
+41 -16
View File
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-empty-function */
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client"; import { PeriodType, Prisma, SchedulingType } from "@prisma/client";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
@@ -16,15 +16,15 @@ import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
import findDurationType from "@calcom/lib/findDurationType"; import findDurationType from "@calcom/lib/findDurationType";
import getStripeAppData from "@calcom/lib/getStripeAppData"; import getStripeAppData from "@calcom/lib/getStripeAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar"; import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar";
import { Form, showToast } from "@calcom/ui"; import { Form, showToast } from "@calcom/ui";
import { asStringOrThrow } from "@lib/asStringOrNull"; import { asStringOrThrow } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab"; import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
@@ -39,6 +39,7 @@ import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayo
import EventWorkflowsTab from "@components/eventtype/EventWorkfowsTab"; import EventWorkflowsTab from "@components/eventtype/EventWorkfowsTab";
import { getTranslation } from "@server/lib/i18n"; import { getTranslation } from "@server/lib/i18n";
import { ssrInit } from "@server/lib/ssr";
export type FormValues = { export type FormValues = {
title: string; title: string;
@@ -63,7 +64,7 @@ export type FormValues = {
displayLocationPublicly?: boolean; displayLocationPublicly?: boolean;
phone?: string; phone?: string;
}[]; }[];
customInputs: EventTypeCustomInput[]; customInputs: CustomInputParsed[];
users: string[]; users: string[];
schedule: number; schedule: number;
periodType: PeriodType; periodType: PeriodType;
@@ -87,6 +88,8 @@ export type FormValues = {
bookingLimits?: BookingLimit; bookingLimits?: BookingLimit;
}; };
export type CustomInputParsed = typeof customInputSchema._output;
const querySchema = z.object({ const querySchema = z.object({
tabName: z tabName: z
.enum(["setup", "availability", "apps", "limits", "recurring", "team", "advanced", "workflows"]) .enum(["setup", "availability", "apps", "limits", "recurring", "team", "advanced", "workflows"])
@@ -150,6 +153,21 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
endDate: new Date(eventType.periodEndDate || Date.now()), endDate: new Date(eventType.periodEndDate || Date.now()),
}); });
const metadata = eventType.metadata;
// fallback to !!eventType.schedule when 'useHostSchedulesForTeamEvent' is undefined
if (!!team) {
metadata.config = {
...metadata.config,
useHostSchedulesForTeamEvent:
typeof eventType.metadata.config?.useHostSchedulesForTeamEvent !== "undefined"
? eventType.metadata.config?.useHostSchedulesForTeamEvent === true
: !!eventType.schedule,
};
} else {
// Make sure non-team events NEVER have this config key;
delete metadata.config?.useHostSchedulesForTeamEvent;
}
const formMethods = useForm<FormValues>({ const formMethods = useForm<FormValues>({
defaultValues: { defaultValues: {
title: eventType.title, title: eventType.title,
@@ -172,17 +190,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
findDurationType(eventType.minimumBookingNotice), findDurationType(eventType.minimumBookingNotice),
eventType.minimumBookingNotice eventType.minimumBookingNotice
), ),
// fallback to !!eventType.schedule when 'useHostSchedulesForTeamEvent' is undefined metadata,
metadata: {
...eventType.metadata,
config: {
...eventType.metadata.config,
useHostSchedulesForTeamEvent:
typeof eventType.metadata.config?.useHostSchedulesForTeamEvent !== "undefined"
? eventType.metadata.config?.useHostSchedulesForTeamEvent === true
: !!eventType.schedule,
},
},
}, },
}); });
@@ -256,6 +264,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
recurringEvent, recurringEvent,
locations, locations,
metadata, metadata,
customInputs,
// We don't need to send send these values to the backend // We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
seatsPerTimeSlotEnabled, seatsPerTimeSlotEnabled,
@@ -266,7 +275,17 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
if (bookingLimits) { if (bookingLimits) {
const isValid = validateBookingLimitOrder(bookingLimits); const isValid = validateBookingLimitOrder(bookingLimits);
if (!isValid) throw new Error("Booking limits must be in accending order. [day,week,month,year]"); if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
}
if (metadata?.multipleDuration !== undefined) {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!metadata?.multipleDuration?.includes(input.length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
} }
updateMutation.mutate({ updateMutation.mutate({
@@ -283,6 +302,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
seatsPerTimeSlot, seatsPerTimeSlot,
seatsShowAttendees, seatsShowAttendees,
metadata, metadata,
customInputs,
}); });
}}> }}>
<div ref={animationParentRef} className="space-y-6"> <div ref={animationParentRef} className="space-y-6">
@@ -297,6 +317,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const { req, query } = context; const { req, query } = context;
const session = await getSession({ req }); const session = await getSession({ req });
const typeParam = parseInt(asStringOrThrow(query.type)); const typeParam = parseInt(asStringOrThrow(query.type));
const ssr = await ssrInit(context);
if (Number.isNaN(typeParam)) { if (Number.isNaN(typeParam)) {
return { return {
@@ -480,6 +501,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
// const parsedMetaData = _EventTypeModel.parse(newMetadata); // const parsedMetaData = _EventTypeModel.parse(newMetadata);
const parsedMetaData = newMetadata; const parsedMetaData = newMetadata;
const parsedCustomInputs = (rawEventType.customInputs || []).map((input) => customInputSchema.parse(input));
const eventType = { const eventType = {
...restEventType, ...restEventType,
schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null, schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null,
@@ -487,6 +510,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
bookingLimits: parseBookingLimit(restEventType.bookingLimits), bookingLimits: parseBookingLimit(restEventType.bookingLimits),
locations: locations as unknown as LocationObject[], locations: locations as unknown as LocationObject[],
metadata: parsedMetaData, metadata: parsedMetaData,
customInputs: parsedCustomInputs,
}; };
// backwards compat // backwards compat
@@ -531,6 +555,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
team: eventTypeObject.team || null, team: eventTypeObject.team || null,
teamMembers, teamMembers,
currentUserMembership, currentUserMembership,
trpcState: ssr.dehydrate(),
}, },
}; };
}; };
+19 -10
View File
@@ -1,10 +1,11 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import Head from "next/head"; import { GetServerSidePropsContext } from "next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { Fragment, useEffect, useState } from "react"; import React, { Fragment, useEffect, useState } from "react";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import CreateEventTypeButton from "@calcom/features/eventtypes/components/CreateEventTypeButton";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react"; import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
import { import {
@@ -12,7 +13,6 @@ import {
Button, Button,
ButtonGroup, ButtonGroup,
ConfirmationDialogContent, ConfirmationDialogContent,
CreateEventTypeButton,
Dialog, Dialog,
Dropdown, Dropdown,
DropdownItem, DropdownItem,
@@ -38,6 +38,8 @@ import SkeletonLoader from "@components/eventtype/SkeletonLoader";
import Avatar from "@components/ui/Avatar"; import Avatar from "@components/ui/Avatar";
import AvatarGroup from "@components/ui/AvatarGroup"; import AvatarGroup from "@components/ui/AvatarGroup";
import { ssrInit } from "@server/lib/ssr";
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"]; type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
type EventTypeGroupProfile = EventTypeGroups[number]["profile"]; type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
@@ -440,7 +442,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
navigator navigator
.share({ .share({
title: t("share"), title: t("share"),
text: t("share_event"), text: t("share_event", { appName: APP_NAME }),
url: calLink, url: calLink,
}) })
.then(() => showToast(t("link_shared"), "success")) .then(() => showToast(t("link_shared"), "success"))
@@ -531,9 +533,9 @@ const EventTypeListHeading = ({
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}> <Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
<a className="font-bold">{profile?.name || ""}</a> <a className="font-bold">{profile?.name || ""}</a>
</Link> </Link>
{membershipCount && ( {membershipCount && teamId && (
<span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 rtl:mr-2"> <span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 rtl:mr-2">
<Link href="/settings/teams"> <Link href={`/settings/teams/${teamId}/members`}>
<a> <a>
<Badge variant="gray"> <Badge variant="gray">
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" /> <Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" />
@@ -579,12 +581,9 @@ const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer);
const EventTypesPage = () => { const EventTypesPage = () => {
const { t } = useLocale(); const { t } = useLocale();
return ( return (
<div> <div>
<Head>
<title>Home | Cal.com</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell <Shell
heading={t("event_types_page_title") as string} heading={t("event_types_page_title") as string}
subtitle={t("event_types_page_subtitle") as string} subtitle={t("event_types_page_subtitle") as string}
@@ -622,4 +621,14 @@ const EventTypesPage = () => {
); );
}; };
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default EventTypesPage; export default EventTypesPage;
@@ -5,20 +5,20 @@ import { useRouter } from "next/router";
import { z } from "zod"; import { z } from "zod";
import { getSession } from "@calcom/lib/auth"; import { getSession } from "@calcom/lib/auth";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { User } from "@calcom/prisma/client"; import { User } from "@calcom/prisma/client";
import { Button, StepCard, Steps } from "@calcom/ui"; import { Button, StepCard, Steps } from "@calcom/ui";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars"; import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability"; import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
import UserProfile from "@components/getting-started/steps-views/UserProfile"; import UserProfile from "@components/getting-started/steps-views/UserProfile";
import { UserSettings } from "@components/getting-started/steps-views/UserSettings"; import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
interface IOnboardingPageProps { export type IOnboardingPageProps = inferSSRProps<typeof getServerSideProps>;
user: User;
}
const INITIAL_STEP = "user-settings"; const INITIAL_STEP = "user-settings";
const steps = ["user-settings", "connected-calendar", "setup-availability", "user-profile"] as const; const steps = ["user-settings", "connected-calendar", "setup-availability", "user-profile"] as const;
@@ -46,7 +46,7 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
const headers = [ const headers = [
{ {
title: `${t("welcome_to_cal_header")}`, title: `${t("welcome_to_cal_header", { appName: APP_NAME })}`,
subtitle: [`${t("we_just_need_basic_info")}`, `${t("edit_form_later_subtitle")}`], subtitle: [`${t("we_just_need_basic_info")}`, `${t("edit_form_later_subtitle")}`],
}, },
{ {
@@ -86,7 +86,9 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
data-testid="onboarding" data-testid="onboarding"
key={router.asPath}> key={router.asPath}>
<Head> <Head>
<title>Cal.com - {t("getting_started")}</title> <title>
{APP_NAME} - {t("getting_started")}
</title>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
+4 -1
View File
@@ -42,6 +42,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!booking) { if (!booking) {
return { return {
notFound: true, notFound: true,
} as {
notFound: true;
}; };
} }
@@ -49,6 +51,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
// TODO: Show something in UI to let user know that this booking is not rescheduleable. // TODO: Show something in UI to let user know that this booking is not rescheduleable.
return { return {
notFound: true, notFound: true,
} as {
notFound: true;
}; };
} }
@@ -62,7 +66,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
: booking.user?.username || "rick") /* This shouldn't happen */ + : booking.user?.username || "rick") /* This shouldn't happen */ +
"/" + "/" +
eventType?.slug; eventType?.slug;
return { return {
redirect: { redirect: {
destination: "/" + eventPage + "?rescheduleUid=" + context.query.uid, destination: "/" + eventPage + "?rescheduleUid=" + context.query.uid,
@@ -0,0 +1,7 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[uid]";
export { default } from "../[uid]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+18 -2
View File
@@ -1,14 +1,30 @@
import { GetServerSidePropsContext } from "next";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getAdminLayout as getLayout, Meta } from "@calcom/ui"; import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
function AdminAppsView() { function AdminAppsView() {
const { t } = useLocale();
return ( return (
<> <>
<Meta title="Apps" description="apps_description" /> <Meta title={t("apps")} description={t("apps_description")} />
<h1>App listing</h1> <h1>{t("apps_listing")}</h1>
</> </>
); );
} }
AdminAppsView.getLayout = getLayout; AdminAppsView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default AdminAppsView; export default AdminAppsView;
@@ -1,16 +1,19 @@
import { GetServerSidePropsContext } from "next";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { useRef } from "react"; import { useRef } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, getAdminLayout as getLayout, Meta, TextField } from "@calcom/ui"; import { Button, getAdminLayout as getLayout, Meta, TextField } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
function AdminView() { function AdminView() {
const { t } = useLocale(); const { t } = useLocale();
const usernameRef = useRef<HTMLInputElement>(null); const usernameRef = useRef<HTMLInputElement>(null);
return ( return (
<> <>
<Meta title="Admin" description="Impersonation" /> <Meta title={t("admin")} description={t("impersonation")} />
<form <form
className="mb-6 w-full sm:w-1/2" className="mb-6 w-full sm:w-1/2"
onSubmit={(e) => { onSubmit={(e) => {
@@ -21,7 +24,7 @@ function AdminView() {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<TextField <TextField
containerClassName="w-full" containerClassName="w-full"
name="Impersonate User" name={t("user_impersonation_heading")}
addOnLeading={<>{process.env.NEXT_PUBLIC_WEBSITE_URL}/</>} addOnLeading={<>{process.env.NEXT_PUBLIC_WEBSITE_URL}/</>}
ref={usernameRef} ref={usernameRef}
hint={t("impersonate_user_tip")} hint={t("impersonate_user_tip")}
@@ -36,4 +39,14 @@ function AdminView() {
AdminView.getLayout = getLayout; AdminView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default AdminView; export default AdminView;
+14
View File
@@ -1,5 +1,9 @@
import { GetServerSidePropsContext } from "next";
import { getAdminLayout as getLayout, Meta } from "@calcom/ui"; import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
function AdminAppsView() { function AdminAppsView() {
return ( return (
<> <>
@@ -11,4 +15,14 @@ function AdminAppsView() {
AdminAppsView.getLayout = getLayout; AdminAppsView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default AdminAppsView; export default AdminAppsView;
+18 -2
View File
@@ -1,14 +1,30 @@
import { GetServerSidePropsContext } from "next";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getAdminLayout as getLayout, Meta } from "@calcom/ui"; import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
function AdminUsersView() { function AdminUsersView() {
const { t } = useLocale();
return ( return (
<> <>
<Meta title="Users" description="users_description" /> <Meta title={t("users")} description={t("users_description")} />
<h1>Users listing</h1> <h1>{t("users_listing")}</h1>
</> </>
); );
} }
AdminUsersView.getLayout = getLayout; AdminUsersView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default AdminUsersView; export default AdminUsersView;
+14 -1
View File
@@ -1,3 +1,4 @@
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import { HelpScout, useChat } from "react-live-chat-loader"; import { HelpScout, useChat } from "react-live-chat-loader";
@@ -8,6 +9,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, getSettingsLayout as getLayout, Icon, Meta } from "@calcom/ui"; import { Button, getSettingsLayout as getLayout, Icon, Meta } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
interface CtaRowProps { interface CtaRowProps {
title: string; title: string;
description: string; description: string;
@@ -64,7 +67,7 @@ const BillingView = () => {
<CtaRow title={t("billing_help_title")} description={t("billing_help_description")}> <CtaRow title={t("billing_help_title")} description={t("billing_help_description")}>
<Button color="secondary" onClick={onContactSupportClick}> <Button color="secondary" onClick={onContactSupportClick}>
{t("billing_help_cta")} {t("contact_support")}
</Button> </Button>
</CtaRow> </CtaRow>
{showChat && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />} {showChat && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
@@ -75,4 +78,14 @@ const BillingView = () => {
BillingView.getLayout = getLayout; BillingView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default BillingView; export default BillingView;
+20 -3
View File
@@ -1,9 +1,11 @@
import { GetServerSidePropsContext } from "next";
import { useState } from "react"; import { useState } from "react";
import { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem"; import { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
import LicenseRequired from "@calcom/ee/common/components/v2/LicenseRequired"; import LicenseRequired from "@calcom/ee/common/components/v2/LicenseRequired";
import ApiKeyDialogForm from "@calcom/features/ee/api-keys/components/ApiKeyDialogForm"; import ApiKeyDialogForm from "@calcom/features/ee/api-keys/components/ApiKeyDialogForm";
import ApiKeyListItem from "@calcom/features/ee/api-keys/components/ApiKeyListItem"; import ApiKeyListItem from "@calcom/features/ee/api-keys/components/ApiKeyListItem";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { import {
@@ -17,6 +19,8 @@ import {
SkeletonLoader, SkeletonLoader,
} from "@calcom/ui"; } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
const ApiKeysView = () => { const ApiKeysView = () => {
const { t } = useLocale(); const { t } = useLocale();
@@ -43,7 +47,10 @@ const ApiKeysView = () => {
return ( return (
<> <>
<Meta title="API Keys" description="API keys allow other apps to communicate with Cal.com" /> <Meta
title={t("api_keys")}
description={t("create_first_api_key_description", { appName: APP_NAME })}
/>
<LicenseRequired> <LicenseRequired>
<> <>
@@ -70,7 +77,7 @@ const ApiKeysView = () => {
<EmptyScreen <EmptyScreen
Icon={Icon.FiLink} Icon={Icon.FiLink}
headline={t("create_first_api_key")} headline={t("create_first_api_key")}
description={t("create_first_api_key_description")} description={t("create_first_api_key_description", { appName: APP_NAME })}
buttonRaw={<NewApiKeyButton />} buttonRaw={<NewApiKeyButton />}
/> />
)} )}
@@ -79,7 +86,7 @@ const ApiKeysView = () => {
</LicenseRequired> </LicenseRequired>
<Dialog open={apiKeyModal} onOpenChange={setApiKeyModal}> <Dialog open={apiKeyModal} onOpenChange={setApiKeyModal}>
<DialogContent type="creation" useOwnActionButtons> <DialogContent type="creation">
<ApiKeyDialogForm handleClose={() => setApiKeyModal(false)} defaultValues={apiKeyToEdit} /> <ApiKeyDialogForm handleClose={() => setApiKeyModal(false)} defaultValues={apiKeyToEdit} />
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -89,4 +96,14 @@ const ApiKeysView = () => {
ApiKeysView.getLayout = getLayout; ApiKeysView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default ApiKeysView; export default ApiKeysView;
@@ -1,5 +1,8 @@
import { GetServerSidePropsContext } from "next";
import { useSession } from "next-auth/react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { import {
@@ -16,9 +19,12 @@ import {
Switch, Switch,
} from "@calcom/ui"; } from "@calcom/ui";
const SkeletonLoader = () => { import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return ( return (
<SkeletonContainer> <SkeletonContainer>
<Meta title={title} description={description} />
<div className="mt-6 mb-8 space-y-6 divide-y"> <div className="mt-6 mb-8 space-y-6 divide-y">
<div className="flex items-center"> <div className="flex items-center">
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" /> <SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
@@ -40,7 +46,7 @@ const SkeletonLoader = () => {
const AppearanceView = () => { const AppearanceView = () => {
const { t } = useLocale(); const { t } = useLocale();
const session = useSession();
const utils = trpc.useContext(); const utils = trpc.useContext();
const { data: user, isLoading } = trpc.viewer.me.useQuery(); const { data: user, isLoading } = trpc.viewer.me.useQuery();
@@ -67,7 +73,7 @@ const AppearanceView = () => {
}, },
}); });
if (isLoading) return <SkeletonLoader />; if (isLoading) return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
if (!user) return null; if (!user) return null;
@@ -84,7 +90,7 @@ const AppearanceView = () => {
theme: values.theme || null, theme: values.theme || null,
}); });
}}> }}>
<Meta title="Appearance" description="Manage settings for your booking appearance" /> <Meta title={t("appearance")} description={t("appearance_description")} />
<div className="mb-6 flex items-center text-sm"> <div className="mb-6 flex items-center text-sm">
<div> <div>
<p className="font-semibold">{t("theme")}</p> <p className="font-semibold">{t("theme")}</p>
@@ -171,14 +177,15 @@ const AppearanceView = () => {
<div className="flex w-full text-sm"> <div className="flex w-full text-sm">
<div className="mr-1 flex-grow"> <div className="mr-1 flex-grow">
<div className="flex items-center"> <div className="flex items-center">
<p className="mr-2 font-semibold">{t("disable_cal_branding")}</p> <p className="mr-2 font-semibold">{t("disable_cal_branding", { appName: APP_NAME })}</p>
<Badge variant="gray">{t("pro")}</Badge> <Badge variant="gray">{t("pro")}</Badge>
</div> </div>
<p className="mt-0.5 text-gray-600">{t("removes_cal_branding")}</p> <p className="mt-0.5 text-gray-600">{t("removes_cal_branding", { appName: APP_NAME })}</p>
</div> </div>
<div className="flex-none"> <div className="flex-none">
<Switch <Switch
id="hideBranding" id="hideBranding"
disabled={!session.data?.user.belongsToActiveTeam}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
formMethods.setValue("hideBranding", checked, { shouldDirty: true }) formMethods.setValue("hideBranding", checked, { shouldDirty: true })
} }
@@ -203,6 +210,16 @@ const AppearanceView = () => {
AppearanceView.getLayout = getLayout; AppearanceView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default AppearanceView; export default AppearanceView;
interface ThemeLabelProps { interface ThemeLabelProps {
variant: "light" | "dark" | "system"; variant: "light" | "dark" | "system";
@@ -1,8 +1,10 @@
import { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Fragment } from "react"; import { Fragment } from "react";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector"; import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
@@ -10,7 +12,6 @@ import {
Alert, Alert,
Badge, Badge,
Button, Button,
DisconnectIntegration,
EmptyScreen, EmptyScreen,
getSettingsLayout as getLayout, getSettingsLayout as getLayout,
Icon, Icon,
@@ -22,12 +23,15 @@ import {
SkeletonButton, SkeletonButton,
SkeletonContainer, SkeletonContainer,
SkeletonText, SkeletonText,
showToast,
} from "@calcom/ui"; } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
import { CalendarSwitch } from "@components/settings/CalendarSwitch"; import { CalendarSwitch } from "@components/settings/CalendarSwitch";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = () => { const SkeletonLoader = () => {
return ( return (
<SkeletonContainer> <SkeletonContainer>
@@ -66,15 +70,17 @@ const CalendarsView = () => {
async onSettled() { async onSettled() {
await utils.viewer.connectedCalendars.invalidate(); await utils.viewer.connectedCalendars.invalidate();
}, },
onSuccess: async () => {
showToast(t("calendar_updated_successfully"), "success");
},
onError: () => {
showToast(t("unexpected_error_try_again"), "error");
},
}); });
return ( return (
<> <>
<Meta <Meta title={t("calendars")} description={t("calendars_description")} CTA={<AddCalendarButton />} />
title="Calendars"
description="Configure how your event types interact with your calendars"
CTA={<AddCalendarButton />}
/>
<QueryCell <QueryCell
query={query} query={query}
customLoader={<SkeletonLoader />} customLoader={<SkeletonLoader />}
@@ -110,7 +116,7 @@ const CalendarsView = () => {
{t("check_for_conflicts")} {t("check_for_conflicts")}
</h4> </h4>
<p className="pb-2 text-sm leading-5 text-gray-600">{t("select_calendars")}</p> <p className="pb-2 text-sm leading-5 text-gray-600">{t("select_calendars")}</p>
<List> <List className="flex flex-col gap-6" noBorderTreatment>
{data.connectedCalendars.map((item) => ( {data.connectedCalendars.map((item) => (
<Fragment key={item.credentialId}> <Fragment key={item.credentialId}>
{item.error && item.error.message && ( {item.error && item.error.message && (
@@ -137,8 +143,8 @@ const CalendarsView = () => {
/> />
)} )}
{item?.error === undefined && item.calendars && ( {item?.error === undefined && item.calendars && (
<ListItem expanded className="flex-col"> <ListItem className="flex-col rounded-md">
<div className="flex w-full flex-1 items-center space-x-3 pb-5 pl-1 pt-1 rtl:space-x-reverse"> <div className="flex w-full flex-1 items-center space-x-3 p-4 rtl:space-x-reverse">
{ {
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
item.integration.logo && ( item.integration.logo && (
@@ -164,7 +170,7 @@ const CalendarsView = () => {
<DisconnectIntegration <DisconnectIntegration
trashIcon trashIcon
credentialId={item.credentialId} credentialId={item.credentialId}
buttonProps={{ size: "icon", color: "secondary" }} buttonProps={{ className: "border border-gray-300" }}
/> />
</div> </div>
</div> </div>
@@ -172,7 +178,7 @@ const CalendarsView = () => {
<p className="px-2 pt-4 text-sm text-neutral-500"> <p className="px-2 pt-4 text-sm text-neutral-500">
{t("toggle_calendars_conflict")} {t("toggle_calendars_conflict")}
</p> </p>
<ul className="space-y-2 px-2 pt-4"> <ul className="space-y-2 p-4">
{item.calendars.map((cal) => ( {item.calendars.map((cal) => (
<CalendarSwitch <CalendarSwitch
key={cal.externalId} key={cal.externalId}
@@ -224,4 +230,14 @@ const CalendarsView = () => {
CalendarsView.getLayout = getLayout; CalendarsView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default CalendarsView; export default CalendarsView;
@@ -1,3 +1,4 @@
import { GetServerSidePropsContext } from "next";
import { useState } from "react"; import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -5,7 +6,9 @@ import { trpc } from "@calcom/trpc/react";
import { import {
Button, Button,
Dialog, Dialog,
DialogClose,
DialogContent, DialogContent,
DialogFooter,
Dropdown, Dropdown,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
@@ -22,9 +25,12 @@ import {
SkeletonText, SkeletonText,
} from "@calcom/ui"; } from "@calcom/ui";
const SkeletonLoader = () => { import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return ( return (
<SkeletonContainer> <SkeletonContainer>
<Meta title={title} description={description} />
<div className="mt-6 mb-8 space-y-6 divide-y"> <div className="mt-6 mb-8 space-y-6 divide-y">
<SkeletonText className="h-8 w-full" /> <SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" /> <SkeletonText className="h-8 w-full" />
@@ -58,18 +64,19 @@ const ConferencingLayout = () => {
const [deleteAppModal, setDeleteAppModal] = useState(false); const [deleteAppModal, setDeleteAppModal] = useState(false);
const [deleteCredentialId, setDeleteCredentialId] = useState<number>(0); const [deleteCredentialId, setDeleteCredentialId] = useState<number>(0);
if (isLoading) return <SkeletonLoader />; if (isLoading)
return <SkeletonLoader title={t("conferencing")} description={t("conferencing_description")} />;
return ( return (
<div className="w-full bg-white sm:mx-0 xl:mt-0"> <div className="w-full bg-white sm:mx-0 xl:mt-0">
<Meta title="Conferencing" description="Add your favourite video conferencing apps for your meetings" /> <Meta title={t("conferencing")} description={t("conferencing_description")} />
<List> <List>
{apps?.items && {apps?.items &&
apps.items apps.items
.map((app) => ({ ...app, title: app.title || app.name })) .map((app) => ({ ...app, title: app.title || app.name }))
.map((app) => ( .map((app) => (
<ListItem className="flex-col border-0" key={app.title}> <ListItem className="flex-col border-0" key={app.title}>
<div className="flex w-full flex-1 items-center space-x-3 pl-1 pt-1 rtl:space-x-reverse"> <div className="flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse">
{ {
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} /> app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} />
@@ -111,10 +118,14 @@ const ConferencingLayout = () => {
title={t("Remove app")} title={t("Remove app")}
description={t("are_you_sure_you_want_to_remove_this_app")} description={t("are_you_sure_you_want_to_remove_this_app")}
type="confirmation" type="confirmation"
actionText={t("yes_remove_app")} Icon={Icon.FiAlertCircle}>
Icon={Icon.FiAlertCircle} <DialogFooter>
actionOnClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })} <Button color="primary" onClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}>
/> {t("yes_remove_app")}
</Button>
<DialogClose />
</DialogFooter>
</DialogContent>
</Dialog> </Dialog>
</div> </div>
); );
@@ -122,4 +133,14 @@ const ConferencingLayout = () => {
ConferencingLayout.getLayout = getLayout; ConferencingLayout.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default ConferencingLayout; export default ConferencingLayout;
+18 -4
View File
@@ -1,3 +1,4 @@
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useMemo } from "react"; import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
@@ -21,9 +22,12 @@ import {
import { withQuery } from "@lib/QueryCell"; import { withQuery } from "@lib/QueryCell";
import { nameOfDay } from "@lib/core/i18n/weekday"; import { nameOfDay } from "@lib/core/i18n/weekday";
const SkeletonLoader = () => { import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return ( return (
<SkeletonContainer> <SkeletonContainer>
<Meta title={title} description={description} />
<div className="mt-6 mb-8 space-y-6 divide-y"> <div className="mt-6 mb-8 space-y-6 divide-y">
<SkeletonText className="h-8 w-full" /> <SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" /> <SkeletonText className="h-8 w-full" />
@@ -47,14 +51,14 @@ const GeneralQueryView = () => {
const { t } = useLocale(); const { t } = useLocale();
const { data: user, isLoading } = trpc.viewer.me.useQuery(); const { data: user, isLoading } = trpc.viewer.me.useQuery();
if (isLoading) return <SkeletonLoader />; if (isLoading) return <SkeletonLoader title={t("general")} description={t("general_description")} />;
if (!user) { if (!user) {
throw new Error(t("something_went_wrong")); throw new Error(t("something_went_wrong"));
} }
return ( return (
<WithQuery <WithQuery
success={({ data }) => <GeneralView user={user} localeProp={data.locale} />} success={({ data }) => <GeneralView user={user} localeProp={data.locale} />}
customLoader={<SkeletonLoader />} customLoader={<SkeletonLoader title={t("general")} description={t("general_description")} />}
/> />
); );
}; };
@@ -127,7 +131,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
weekStart: values.weekStart.value, weekStart: values.weekStart.value,
}); });
}}> }}>
<Meta title="General" description="Manage settings for your language and timezone" /> <Meta title={t("general")} description={t("general_description")} />
<Controller <Controller
name="locale" name="locale"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
@@ -210,4 +214,14 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
GeneralQueryView.getLayout = getLayout; GeneralQueryView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default GeneralQueryView; export default GeneralQueryView;
+47 -33
View File
@@ -1,10 +1,12 @@
import { IdentityProvider } from "@prisma/client"; import { IdentityProvider } from "@prisma/client";
import crypto from "crypto"; import crypto from "crypto";
import { GetServerSidePropsContext } from "next";
import { signOut } from "next-auth/react"; import { signOut } from "next-auth/react";
import { BaseSyntheticEvent, useEffect, useRef, useState } from "react"; import { BaseSyntheticEvent, useEffect, useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth"; import { ErrorCode } from "@calcom/lib/auth";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { TRPCClientErrorLike } from "@calcom/trpc/client"; import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
@@ -14,7 +16,9 @@ import {
Avatar, Avatar,
Button, Button,
Dialog, Dialog,
DialogClose,
DialogContent, DialogContent,
DialogFooter,
DialogTrigger, DialogTrigger,
Form, Form,
getSettingsLayout as getLayout, getSettingsLayout as getLayout,
@@ -32,11 +36,14 @@ import {
} from "@calcom/ui"; } from "@calcom/ui";
import TwoFactor from "@components/auth/TwoFactor"; import TwoFactor from "@components/auth/TwoFactor";
import { UsernameAvailability } from "@components/ui/UsernameAvailability"; import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
const SkeletonLoader = () => { import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return ( return (
<SkeletonContainer> <SkeletonContainer>
<Meta title={title} description={description} />
<div className="mt-6 mb-8 space-y-6 divide-y"> <div className="mt-6 mb-8 space-y-6 divide-y">
<div className="flex items-center"> <div className="flex items-center">
<SkeletonAvatar className=" h-12 w-12 px-4" /> <SkeletonAvatar className=" h-12 w-12 px-4" />
@@ -205,11 +212,14 @@ const ProfileView = () => {
showToast(t("error_updating_settings"), "error"); showToast(t("error_updating_settings"), "error");
}; };
if (isLoading || !user) return <SkeletonLoader />; if (isLoading || !user)
return <SkeletonLoader title={t("profile")} description={t("profile_description")} />;
const isDisabled = isSubmitting || !isDirty; const isDisabled = isSubmitting || !isDirty;
return ( return (
<> <>
<Meta title={t("profile")} description={t("profile_description", { appName: APP_NAME })} />
<Form <Form
form={formMethods} form={formMethods}
handleSubmit={(values) => { handleSubmit={(values) => {
@@ -219,7 +229,6 @@ const ProfileView = () => {
mutation.mutate(values); mutation.mutate(values);
} }
}}> }}>
<Meta title="Profile" description="Manage settings for your cal profile" />
<div className="flex items-center"> <div className="flex items-center">
<Controller <Controller
control={formMethods.control} control={formMethods.control}
@@ -233,7 +242,7 @@ const ProfileView = () => {
id="avatar-upload" id="avatar-upload"
buttonMsg={t("change_avatar")} buttonMsg={t("change_avatar")}
handleAvatarChange={(newAvatar) => { handleAvatarChange={(newAvatar) => {
formMethods.setValue("avatar", newAvatar); formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
}} }}
imageSrc={value} imageSrc={value}
/> />
@@ -243,22 +252,10 @@ const ProfileView = () => {
/> />
</div> </div>
<div className="mt-8"> <div className="mt-8">
<Controller <UsernameAvailabilityField
control={formMethods.control} user={user}
name="username" onSuccessMutation={onSuccessfulUsernameUpdate}
render={({ field: { ref, onChange, value } }) => { onErrorMutation={onErrorInUsernameUpdate}
return (
<UsernameAvailability
currentUsername={user?.username || ""}
inputUsernameValue={value}
usernameRef={ref}
setInputUsernameValue={onChange}
onSuccessMutation={onSuccessfulUsernameUpdate}
onErrorMutation={onErrorInUsernameUpdate}
user={user}
/>
);
}}
/> />
</div> </div>
<div className="mt-8"> <div className="mt-8">
@@ -296,17 +293,11 @@ const ProfileView = () => {
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
title={t("delete_account_modal_title")} title={t("delete_account_modal_title")}
description={t("confirm_delete_account_modal")} description={t("confirm_delete_account_modal", { appName: APP_NAME })}
type="creation" type="creation"
actionText={t("delete_my_account")} Icon={Icon.FiAlertTriangle}>
actionProps={{
// @ts-expect-error data attributes aren't typed
"data-testid": "delete-account-confirm",
}}
Icon={Icon.FiAlertTriangle}
actionOnClick={(e) => e && onConfirmButton(e)}>
<> <>
<p className="mb-7">{t("delete_account_confirmation_message")}</p> <p className="mb-7">{t("delete_account_confirmation_message", { appName: APP_NAME })}</p>
{isCALIdentityProviver && ( {isCALIdentityProviver && (
<PasswordField <PasswordField
data-testid="password" data-testid="password"
@@ -326,6 +317,15 @@ const ProfileView = () => {
)} )}
{hasDeleteErrors && <Alert severity="error" title={deleteErrorMessage} />} {hasDeleteErrors && <Alert severity="error" title={deleteErrorMessage} />}
<DialogFooter>
<Button
color="primary"
data-testid="delete-account-confirm"
onClick={(e) => onConfirmButton(e)}>
{t("delete_my_account")}
</Button>
<DialogClose />
</DialogFooter>
</> </>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -337,9 +337,7 @@ const ProfileView = () => {
title={t("confirm_password")} title={t("confirm_password")}
description={t("confirm_password_change_email")} description={t("confirm_password_change_email")}
type="creation" type="creation"
actionText={t("confirm")} Icon={Icon.FiAlertTriangle}>
Icon={Icon.FiAlertTriangle}
actionOnClick={(e) => e && onConfirmPassword(e)}>
<> <>
<PasswordField <PasswordField
data-testid="password" data-testid="password"
@@ -352,6 +350,12 @@ const ProfileView = () => {
/> />
{confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />} {confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />}
<DialogFooter>
<Button color="primary" onClick={(e) => onConfirmPassword(e)}>
{t("confirm")}
</Button>
<DialogClose />
</DialogFooter>
</> </>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -361,4 +365,14 @@ const ProfileView = () => {
ProfileView.getLayout = getLayout; ProfileView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default ProfileView; export default ProfileView;
@@ -1,3 +1,4 @@
import { GetServerSidePropsContext } from "next";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -13,6 +14,8 @@ import {
Switch, Switch,
} from "@calcom/ui"; } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
const ProfileImpersonationView = () => { const ProfileImpersonationView = () => {
const { t } = useLocale(); const { t } = useLocale();
const utils = trpc.useContext(); const utils = trpc.useContext();
@@ -39,7 +42,7 @@ const ProfileImpersonationView = () => {
return ( return (
<> <>
<Meta title="Impersonation Settings" description="" /> <Meta title={t("impersonation")} description={t("impersonation_description")} />
<Form <Form
form={formMethods} form={formMethods}
handleSubmit={({ disableImpersonation }) => { handleSubmit={({ disableImpersonation }) => {
@@ -74,4 +77,14 @@ const ProfileImpersonationView = () => {
ProfileImpersonationView.getLayout = getLayout; ProfileImpersonationView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default ProfileImpersonationView; export default ProfileImpersonationView;
@@ -1,4 +1,5 @@
import { IdentityProvider } from "@prisma/client"; import { IdentityProvider } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -7,6 +8,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, Form, getSettingsLayout as getLayout, Meta, PasswordField, showToast } from "@calcom/ui"; import { Button, Form, getSettingsLayout as getLayout, Meta, PasswordField, showToast } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
type ChangePasswordFormValues = { type ChangePasswordFormValues = {
oldPassword: string; oldPassword: string;
newPassword: string; newPassword: string;
@@ -92,4 +95,14 @@ const PasswordView = () => {
PasswordView.getLayout = getLayout; PasswordView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default PasswordView; export default PasswordView;
+14
View File
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/sso/page/user-sso-view"; export { default } from "@calcom/features/ee/sso/page/user-sso-view";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
@@ -1,12 +1,37 @@
import { GetServerSidePropsContext } from "next";
import { useState } from "react"; import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Badge, getSettingsLayout as getLayout, Loader, Meta, Switch } from "@calcom/ui"; import {
Badge,
getSettingsLayout as getLayout,
Meta,
Switch,
SkeletonButton,
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal"; import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal"; import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>
<Meta title={title} description={description} />
<div className="mt-6 mb-8 space-y-6 divide-y">
<div className="flex items-center">
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
<SkeletonText className="h-8 w-full" />
</div>
</div>
</SkeletonContainer>
);
};
const TwoFactorAuthView = () => { const TwoFactorAuthView = () => {
const utils = trpc.useContext(); const utils = trpc.useContext();
@@ -16,11 +41,11 @@ const TwoFactorAuthView = () => {
const [enableModalOpen, setEnableModalOpen] = useState(false); const [enableModalOpen, setEnableModalOpen] = useState(false);
const [disableModalOpen, setDisableModalOpen] = useState(false); const [disableModalOpen, setDisableModalOpen] = useState(false);
if (isLoading) return <Loader />; if (isLoading) return <SkeletonLoader title={t("2fa")} description={t("2fa_description")} />;
return ( return (
<> <>
<Meta title="Two-Factor Authentication" description="Manage settings for your account passwords" /> <Meta title={t("2fa")} description={t("2fa_description")} />
<div className="mt-6 flex items-start space-x-4"> <div className="mt-6 flex items-start space-x-4">
<Switch <Switch
checked={user?.twoFactorEnabled} checked={user?.twoFactorEnabled}
@@ -35,7 +60,7 @@ const TwoFactorAuthView = () => {
{user?.twoFactorEnabled ? t("enabled") : t("disabled")} {user?.twoFactorEnabled ? t("enabled") : t("disabled")}
</Badge> </Badge>
</div> </div>
<p className="text-sm text-gray-600">Add an extra layer of security to your account.</p> <p className="text-sm text-gray-600">{t("add_an_extra_layer_of_security")}</p>
</div> </div>
</div> </div>
@@ -68,4 +93,14 @@ const TwoFactorAuthView = () => {
TwoFactorAuthView.getLayout = getLayout; TwoFactorAuthView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default TwoFactorAuthView; export default TwoFactorAuthView;
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/teams/pages/team-appearance-view"; export { default } from "@calcom/features/ee/teams/pages/team-appearance-view";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/teams/pages/team-billing-view"; export { default } from "@calcom/features/ee/teams/pages/team-billing-view";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/teams/pages/team-members-view"; export { default } from "@calcom/features/ee/teams/pages/team-members-view";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/teams/pages/team-profile-view"; export { default } from "@calcom/features/ee/teams/pages/team-profile-view";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/sso/page/teams-sso-view"; export { default } from "@calcom/features/ee/sso/page/teams-sso-view";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
+17 -2
View File
@@ -1,14 +1,19 @@
import { GetServerSidePropsContext } from "next";
import Head from "next/head"; import Head from "next/head";
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components"; import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getWizardLayout as getLayout } from "@calcom/ui"; import { getWizardLayout as getLayout } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
const CreateNewTeamPage = () => { const CreateNewTeamPage = () => {
const { t } = useLocale();
return ( return (
<> <>
<Head> <Head>
<title>Create a new Team</title> <title>{t("create_new_team")}</title>
<meta name="description" content="Create a new team to ease your organisational booking" /> <meta name="description" content={t("create_new_team_description")} />
</Head> </Head>
<CreateANewTeamForm /> <CreateANewTeamForm />
</> </>
@@ -17,4 +22,14 @@ const CreateNewTeamPage = () => {
CreateNewTeamPage.getLayout = getLayout; CreateNewTeamPage.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default CreateNewTeamPage; export default CreateNewTeamPage;
+2
View File
@@ -43,6 +43,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
name: true, name: true,
slug: true, slug: true,
logo: true, logo: true,
hideBranding: true,
eventTypes: { eventTypes: {
where: { where: {
slug: typeParam, slug: typeParam,
@@ -160,6 +161,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
previousPage: context.req.headers.referer ?? null, previousPage: context.req.headers.referer ?? null,
booking, booking,
trpcState: ssg.dehydrate(), trpcState: ssg.dehydrate(),
isBrandingHidden: team.hideBranding,
}, },
}; };
}; };
+3 -1
View File
@@ -4,6 +4,7 @@ import { JSONObject } from "superjson/dist/types";
import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations"; import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull"; import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import getBooking, { GetBookingType } from "@lib/getBooking"; import getBooking, { GetBookingType } from "@lib/getBooking";
@@ -92,9 +93,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventTypeObject = [eventType].map((e) => { const eventTypeObject = [eventType].map((e) => {
return { return {
...e, ...e,
metadata: (eventType.metadata || {}) as JSONObject, metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
periodStartDate: e.periodStartDate?.toString() ?? null, periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null, periodEndDate: e.periodEndDate?.toString() ?? null,
customInputs: customInputSchema.array().parse(e.customInputs || []),
users: eventType.users.map((u) => ({ users: eventType.users.map((u) => ({
id: u.id, id: u.id,
name: u.name, name: u.name,
+14
View File
@@ -1,8 +1,12 @@
import { GetServerSidePropsContext } from "next";
import { TeamsListing } from "@calcom/features/ee/teams/components"; import { TeamsListing } from "@calcom/features/ee/teams/components";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, Shell } from "@calcom/ui"; import { Button, Icon, Shell } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
function Teams() { function Teams() {
const { t } = useLocale(); const { t } = useLocale();
return ( return (
@@ -22,4 +26,14 @@ function Teams() {
Teams.requiresLicense = false; Teams.requiresLicense = false;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default Teams; export default Teams;
+14
View File
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/workflows/pages/index"; export { default } from "@calcom/features/ee/workflows/pages/index";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
+4 -10
View File
@@ -93,7 +93,7 @@ test.describe("pro user", () => {
await page.locator('[data-testid="confirm-reschedule-button"]').click(); await page.locator('[data-testid="confirm-reschedule-button"]').click();
await page.waitForNavigation({ await page.waitForNavigation({
url(url) { url(url) {
return url.pathname === "/success"; return url.pathname.startsWith("/booking");
}, },
}); });
}); });
@@ -108,20 +108,14 @@ test.describe("pro user", () => {
await page.locator('[data-testid="cancel"]').first().click(); await page.locator('[data-testid="cancel"]').first().click();
await page.waitForNavigation({ await page.waitForNavigation({
url: (url) => { url: (url) => {
return url.pathname.startsWith("/success"); return url.pathname.startsWith("/booking");
}, },
}); });
// --- fill form
await page.locator('[data-testid="cancel"]').click(); await page.locator('[data-testid="cancel"]').click();
await page.waitForNavigation({ const cancelledHeadline = await page.locator('[data-testid="cancelled-headline"]').innerText();
url: (url) => {
return url.pathname.startsWith("/success");
},
});
const successHeadling = await page.locator('[data-testid="success-headline"]').innerText();
await expect(successHeadling).toBe("This event is cancelled"); await expect(cancelledHeadline).toBe("This event is cancelled");
await page.goto(`/${pro.username}`); await page.goto(`/${pro.username}`);
await bookFirstEvent(page); await bookFirstEvent(page);
+5 -67
View File
@@ -1,8 +1,7 @@
import { expect } from "@playwright/test"; import { expect } from "@playwright/test";
import { UserPlan } from "@prisma/client"; import { UserPlan } from "@prisma/client";
import { getFreePlanPrice, getProPlanPrice } from "@calcom/app-store/stripepayment/lib/utils"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
import dayjs from "@calcom/dayjs";
import stripe from "@calcom/features/ee/payments/server/stripe"; import stripe from "@calcom/features/ee/payments/server/stripe";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -27,7 +26,7 @@ test.describe("Change username on settings", () => {
/** TODO: Find out why it's timing out */ /** TODO: Find out why it's timing out */
test.fixme("User can change username", async ({ page, users, prisma }) => { test.fixme("User can change username", async ({ page, users, prisma }) => {
const user = await users.create({ plan: UserPlan.TRIAL }); const user = await users.create();
await user.login(); await user.login();
// Try to go homepage // Try to go homepage
@@ -50,28 +49,13 @@ test.describe("Change username on settings", () => {
expect(newUpdatedUser.username).toBe("demousernamex"); expect(newUpdatedUser.username).toBe("demousernamex");
}); });
test("User trial can update to PREMIUM username", async ({ page, users }, testInfo) => { test("User can update to PREMIUM username", async ({ page, users }, testInfo) => {
// eslint-disable-next-line playwright/no-skipped-test // eslint-disable-next-line playwright/no-skipped-test
test.skip(!IS_STRIPE_ENABLED, "It should only run if Stripe is installed"); test.skip(!IS_STRIPE_ENABLED, "It should only run if Stripe is installed");
test.skip(IS_SELF_HOSTED, "It shouldn't run on self hosted"); test.skip(IS_SELF_HOSTED, "It shouldn't run on self hosted");
const user = await users.create({ plan: UserPlan.TRIAL }); const user = await users.create();
const customer = await stripe.customers.create({ email: `${user?.username}@example.com` }); await stripe.customers.create({ email: `${user?.username}@example.com` });
await stripe.subscriptionSchedules.create({
customer: customer.id,
start_date: "now",
end_behavior: "release",
phases: [
{
items: [{ price: getProPlanPrice() }],
trial_end: dayjs().add(14, "day").unix(),
end_date: dayjs().add(14, "day").unix(),
},
{
items: [{ price: getFreePlanPrice() }],
},
],
});
await user.login(); await user.login();
await page.goto("/settings/my-account/profile"); await page.goto("/settings/my-account/profile");
@@ -97,50 +81,4 @@ test.describe("Change username on settings", () => {
await expect(page).toHaveURL(/.*checkout.stripe.com/); await expect(page).toHaveURL(/.*checkout.stripe.com/);
}); });
test("User PRO can update to PREMIUM username", async ({ page, users }, testInfo) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(!IS_STRIPE_ENABLED, "It should only run if Stripe is installed");
test.skip(IS_SELF_HOSTED, "It shouldn't run on self hosted");
const user = await users.create({ plan: UserPlan.PRO });
const customer = await stripe.customers.create({ email: `${user?.username}@example.com` });
const paymentMethod = await stripe.paymentMethods.create({
type: "card",
card: {
number: "4242424242424242",
cvc: "123",
exp_month: 12,
exp_year: 2040,
},
});
await stripe.paymentMethods.attach(paymentMethod.id, { customer: customer.id });
await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: getProPlanPrice() }],
});
await user.login();
await page.goto("/settings/profile");
// Change username from normal to premium
const usernameInput = page.locator("[data-testid=username-input]");
await usernameInput.fill(`xx${testInfo.workerIndex}`);
// Click on save button
await page.click('button[type="submit"]');
// Validate modal text fields
const currentUsernameText = page.locator("[data-testid=current-username]").innerText();
const newUsernameText = page.locator("[data-testid=new-username]").innerText();
expect(currentUsernameText).not.toBe(newUsernameText);
// Click on Go to billing
await page.click("[data-testid=go-to-billing]", { timeout: 300 });
await page.waitForLoadState();
await expect(page).toHaveURL(/.*billing.stripe.com/);
});
}); });
@@ -37,7 +37,7 @@ test("dynamic booking", async ({ page, users }) => {
await page.locator('[data-testid="confirm-reschedule-button"]').click(); await page.locator('[data-testid="confirm-reschedule-button"]').click();
await page.waitForNavigation({ await page.waitForNavigation({
url(url) { url(url) {
return url.pathname === "/success"; return url.pathname.startsWith("/booking");
}, },
}); });
await expect(page.locator("[data-testid=success-page]")).toBeVisible(); await expect(page.locator("[data-testid=success-page]")).toBeVisible();
@@ -48,20 +48,14 @@ test("dynamic booking", async ({ page, users }) => {
await page.locator('[data-testid="cancel"]').first().click(); await page.locator('[data-testid="cancel"]').first().click();
await page.waitForNavigation({ await page.waitForNavigation({
url: (url) => { url: (url) => {
return url.pathname.startsWith("/success"); return url.pathname.startsWith("/booking");
}, },
}); });
// --- fill form
await page.locator('[data-testid="cancel"]').click(); await page.locator('[data-testid="cancel"]').click();
await page.waitForNavigation({ const cancelledHeadline = await page.locator('[data-testid="cancelled-headline"]').innerText();
url: (url) => {
return url.pathname.startsWith("/success");
},
});
const successHeadling = await page.locator('[data-testid="success-headline"]').innerText();
await expect(successHeadling).toBe("This event is cancelled"); await expect(cancelledHeadline).toBe("This event is cancelled");
}); });
await users.deleteAll(); await users.deleteAll();
+1 -1
View File
@@ -112,7 +112,7 @@ async function bookEventOnThisPage(page: Page) {
// Make sure we're navigated to the success page // Make sure we're navigated to the success page
await page.waitForNavigation({ await page.waitForNavigation({
url(url) { url(url) {
return url.pathname.endsWith("/success"); return url.pathname.startsWith("/booking");
}, },
}); });
await expect(page.locator("[data-testid=success-page]")).toBeVisible(); await expect(page.locator("[data-testid=success-page]")).toBeVisible();
+3 -3
View File
@@ -150,7 +150,7 @@ test.describe("Reschedule Tests", async () => {
await page.locator('[data-testid="confirm-reschedule-button"]').click(); await page.locator('[data-testid="confirm-reschedule-button"]').click();
await expect(page).toHaveURL(/.*success/); await expect(page).toHaveURL(/.*booking/);
await payment.delete(); await payment.delete();
}); });
@@ -168,7 +168,7 @@ test.describe("Reschedule Tests", async () => {
await page.locator('[data-testid="confirm-reschedule-button"]').click(); await page.locator('[data-testid="confirm-reschedule-button"]').click();
await expect(page).toHaveURL(/.*success/); await expect(page).toHaveURL(/.*booking/);
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } }); const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } });
expect(newBooking).not.toBeNull(); expect(newBooking).not.toBeNull();
@@ -189,7 +189,7 @@ test.describe("Reschedule Tests", async () => {
await page.locator('[data-testid="confirm-reschedule-button"]').click(); await page.locator('[data-testid="confirm-reschedule-button"]').click();
await expect(page).toHaveURL(/.*success/); await expect(page).toHaveURL(/.*booking/);
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } }); const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } });
expect(newBooking).not.toBeNull(); expect(newBooking).not.toBeNull();
+1 -1
View File
@@ -35,7 +35,7 @@ test.describe("Teams", () => {
await page.locator('[placeholder="email\\@example\\.com"]').fill(inviteeEmail); await page.locator('[placeholder="email\\@example\\.com"]').fill(inviteeEmail);
// Click [data-testid="invite-new-member-button"] // Click [data-testid="invite-new-member-button"]
await page.locator('[data-testid="invite-new-member-button"]').click(); await page.locator('[data-testid="invite-new-member-button"]').click();
await expect(page.locator(`li:has-text("${inviteeEmail}PendingMemberNot on Cal.com")`)).toBeVisible(); await expect(page.locator(`li:has-text("${inviteeEmail}")`)).toBeVisible();
expect(await page.locator('[data-testid="pending-member-item"]').count()).toBe(2); expect(await page.locator('[data-testid="pending-member-item"]').count()).toBe(2);
}); });
+2
View File
@@ -65,6 +65,8 @@ test("add webhook & test that creating an event triggers a webhook call", async
body.payload.additionalInformation = dynamic; body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic; body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic; body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
// if we change the shape of our webhooks, we can simply update this by clicking `u` // if we change the shape of our webhooks, we can simply update this by clicking `u`
// console.log("BODY", body); // console.log("BODY", body);
@@ -1 +1 @@
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}} {"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","videoCallData":"[redacted/dynamic]","appsStatus":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}}
Loaded 100 of 283 files, more files were not shown because too many files have changed in this diff. Show more