Compare commits

...
109 Commits
Author SHA1 Message Date
Alex van Andel 8ecfe89a6f v2.1.2 2022-11-01 11:31:02 +00:00
b1ced10626 Added generic image variant for og images and make meta description longer. (#5285)
* Added generic image variant for og images.

* Made og and meta descriptions longer, removed difference between longer and shorter descriptions now we only have one variant.

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-01 08:16:08 +00:00
b3ba89c074 Fix avatar infinite redirect (#5299)
* If due to some reason avatar URL is same as route, avoid infinite redirection by serving default

* Fix avatar reverting issue

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-01 06:30:08 +00:00
77d339ae8f Without checkout session premium username isnt possbole (#5296)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-10-31 23:07:51 +00:00
alannnc ac9b2d0577 Feature/invalid credentials (#5120)
* Fixing types from handleErrorJson usage and Credential

* Replace credential prisma type for a better suitable

* Improvements on zoom video adapter

* Renamed extendedCredentialType and put it in a best suited file

* Frontend display invalid credential

* Fix styles and text

* Fix type required for fake daily credentials
2022-10-31 22:06:03 +00:00
Udit Takkar 03fd546e99 fix: add new url (#5295) 2022-10-31 18:53:59 +00:00
Alex van Andel 6484f1639c Default to null for now (#5302) 2022-10-31 18:42:45 +00:00
Hariom BalharaandAlex van Andel f934dc6f4f Fix (#5293)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-31 13:58:47 +00:00
Hariom Balhara dd9d32815a Ensure position is present (#5291) 2022-10-31 12:13:04 +00:00
Jeroen Reumkens 5c2ad7827e Removed old v1 app components in web. (#5286) 2022-10-31 10:42:01 +00:00
Jeroen ReumkensandPeer Richelsen 5dc5eadf28 Allow og image api in robots.txt (#5283)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-31 09:48:17 +00:00
Peer Richelsen d6fc551950 follow up: 4978 cal 87 re add tip in sidebar (#5016) 2022-10-31 08:11:24 +00:00
5bb868d2c3 React embed: fix modal close on click outside (#5181)
* fix: modal close on click outside

* fix: for sm viewport

* add: main class

Co-authored-by: root <root@pop-os.localdomain>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-31 07:57:09 +00:00
Alex van Andel ec9b3a4261 Fixes #5278 - wrong profile picture (#5279)
* Fixes #5278 - wrong profile picture

* users is not allowed to be undefined
2022-10-30 20:31:21 +00:00
Alex van Andel ce04f78a56 Fixed regression introduced when date.format() was removed (#5265) 2022-10-28 20:13:15 +01:00
Carina WollendorferandCarinaWolli 9f362db60c add attendee's email as dynamic text variable (#5258)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-10-28 09:53:06 +00:00
alannncandkodiakhq[bot] 7a93bec735 Add try-catch and validation to run triggers on reschedule (#5252)
* Add try-catch and validation to run triggers on reschedule

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

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-28 09:36:30 +00:00
7b1e824a36 Gegt only warn,error and fatal (#5259)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-28 09:19:48 +00:00
0746b1e372 Fix Zapier sample data and payload (#5241)
* fix zapier example and payload

* fix type error

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-28 09:09:15 +00:00
github-actions[bot]andCrowdin Bot 39747a151c New Crowdin translations by Github Action (#5243)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-28 09:56:09 +01:00
b6ae4529e9 Adds number for sms notifications from workflows to /success (#5210)
* add sms notification phone number to success page

* remove duplicate translation

* add non breaking space between sms and notifications

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-10-27 09:53:13 +00:00
Nafees Nazikandkodiakhq[bot] 8c76da8d79 fix: make bookings nav item selected, while switching to other filter tabs. (#5176)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-27 09:45:01 +00:00
Hariom Balharaandkodiakhq[bot] 4210f35e9c Add a test that could have caught bug #5142 (#5157)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-27 15:04:34 +05:30
+9 eae60043bb [CAL-183] email and calendar still use old “cancel/reschedule” screens (#5149)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
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: MuriloAmarals <muralha2000@gmail.com>
Co-authored-by: Murilo Amaral <87545137+MuriloAmarals@users.noreply.github.com>
Co-authored-by: Rubens Rafael <70234898+RubensRafael@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: Matheus Muniz <matheusmuniz100@hotmail.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: C000Ldude <coolmagnas@gmail.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Eman <emmanuelgatwech@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-27 09:13:52 +00:00
Nafees Nazik bf34296864 fix: truncte longer text (#5150) 2022-10-27 08:22:03 +00:00
github-actions[bot] d0a24f7c3c New Crowdin translations by Github Action (#5215) 2022-10-27 09:07:54 +01:00
Jeroen Reumkens f009321deb Stop long ursl from overflowing on booking page. (#5240) 2022-10-27 09:04:33 +01:00
4d04a4d9a6 add organizer as reply-to email (#5203)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-27 07:05:11 +00:00
Peer Richelsen 888f60a94c added sirius video app (#5187) 2022-10-27 06:06:53 +00:00
d6c4970bf4 Cleaned up v1 for app store pages. (#5204)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-27 05:50:16 +00:00
Hariom Balharaandalannnc 63ce4a68ac Make link relative to root (#5218)
Co-authored-by: alannnc <alannnc@gmail.com>
2022-10-26 21:29:09 +00:00
cddcd742f6 Add: copy link option (#5236)
* add: new item and local var

* add: copy link func

Co-authored-by: root <root@pop-os.localdomain>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-10-26 21:12:25 +00:00
alannnc a7a82a6b51 Fix/event location when fallback video (#5216) 2022-10-26 21:16:38 +01:00
Udit TakkarandPeer Richelsen 1a104f172d fix: use max-w for description (#5237)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-26 19:27:28 +00:00
142be70b89 fix: overflow text, ui bugs (#5071)
* fix: overflow text, ui bugs

* fix: merge conflict

* fix: availability buttons ui

* fix: mobile view added for /event-type header

* fix: dropdown closing bug on mobile

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2022-10-26 16:29:01 +01:00
Peer Richelsen 5a0e4c8726 removed link from cal video (#5228)
* removed link from cal video

* nit
2022-10-26 14:31:19 +01:00
Syed Ali ShahbazandAlex van Andel f2cdb75f91 Hotfix/caldav reschedule jsonerror (#5193)
* fixed reschedule icalstring and json error

* logging update

* code quality changes

* removed unnecessary comment

* removes console log remnants

* fix for checktype

* Take care of all OK status response codes

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

* Add relevant comment in code

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

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-26 11:04:36 +00:00
352ce35b40 fix updating already deleted booking (#5201)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-10-25 19:16:16 +00:00
Leo Giovanetti 3fa52b0600 Fix add calendar button (#5212) 2022-10-25 12:50:19 -06:00
Carina Wollendorfer a4ff5d0cee fixes google calendar issue (#5205) 2022-10-25 13:45:43 +00:00
sean-brydon 5f1d20319a Segment Control (#5170)
* Segment

* Fix type errors

* Fix HUG
2022-10-25 10:47:03 +00:00
Jeroen Reumkens d597aa91e5 Fixed alignment for CTA buttons in PWA. (#5173) 2022-10-25 08:51:43 +00:00
github-actions[bot] 803ab6c2e9 New Crowdin translations by Github Action (#5174) 2022-10-25 09:41:29 +01:00
Hariom Balhara 0a23be4579 Ensure the URL is absolute before checking the origin (#5194) 2022-10-25 13:48:19 +05:30
Hariom Balharaandkodiakhq[bot] 00c3e9680c Fix subjects for emails for Optin Bookings (#5148)
* Fix subject for manually confirmed booking

* Fix subject for request email to attendee

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-25 07:32:05 +00:00
zomars 68ae3c48a0 v2.1.1 2022-10-24 18:41:47 -06:00
alannncandOmar López 1c59c0dd06 try catch all the promises after reservation its completed (#5182)
Co-authored-by: Omar López <zomars@me.com>
2022-10-24 18:32:14 -06:00
2d5bf1ffc1 feat: new route added for deletion (#5160)
* feat: new route added for deletion
required password removed from update profile

* fix: update url

Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-24 18:32:00 -06:00
Joe Au-Yeung 317909d20a Fix 404 error when editing default availability & create new availability from event type page (#5085)
* Edit/create new availability

* Connect new schedule to event type

* Remove comment

* Shell back button executes window.history.back

* Fix default schedule

* Add default badge

* Fix type error
2022-10-24 18:29:49 -06:00
Jeroen ReumkensandPeer Richelsen 16eb516a9b Added improved designs for OG images. (#5102)
* Added improved designs for OG images.

* Changed size of additional attendees circle in og image.

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-24 18:20:01 -06:00
Jeroen ReumkensandOmar López a0e93e1eeb Feature/improved timeformat selection booking page (#5175)
* Aded new togglegroup component.

* Make 12/24h time format toggle functional and store value in localstorage.

* Responsive tweaks

* Added translations

* Added 12/24h timeformat hint on profile page.

* Update apps/web/public/static/locales/en/common.json

Co-authored-by: Omar López <zomars@me.com>
2022-10-24 18:14:06 -06:00
sean-brydonandOmar López b18eabc6e8 #5131 -Event Type Settings Inconsistency (#5147)
* Reusable component

* Fix limits not being toggleable

* Remove custom input margin

* addTestId

* Limits+adv

* Reccuring Tab

* Remove console .log

Co-authored-by: Omar López <zomars@me.com>
2022-10-24 17:11:07 -06:00
Alex van AndelandLeo Giovanetti 5305f31266 Bugfix/recurring dst change (#5172)
* Structural fix to recurring times

* Remove conversion regression

* Revert current time -> startTime based utcOffset

* Fixing remaining events count

* Using user's preference for recurring tooltip

* Missing refactor

* Showing better datetime in booking page
Added an extra seeded example for recurring

Co-authored-by: Leo Giovanetti <hello@leog.me>
2022-10-24 16:37:55 -06:00
sean-brydonandalannnc 6232a111ef Empty Screen Improvements to Public event type list (#5178)
* Empty Page

* Declare const and reuse it

Co-authored-by: alannnc <alannnc@gmail.com>
2022-10-24 22:20:41 +00:00
0ea58103c8 Harsh/fix layout shift on loading ss (#5184)
* fix: layout shift in ss apps page

* Fix for mobile style justify-center

Co-authored-by: root <root@pop-os.localdomain>
Co-authored-by: Alan <alannnc@gmail.com>
2022-10-24 21:36:19 +00:00
3d595e9299 Fix design of showing generated API key (#5180)
* fix design of api key

* fix copy icon

* Fix copy key height

* Add mobile style fixes

* Fix styles

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
2022-10-24 21:13:27 +00:00
f135f7baad Fix unauthorized error when saving workflows (#5127)
* fix wrong unauthorized error when no userId and teamId

* use find instead of filter

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-24 09:38:05 +02:00
Peer RichelsenandHariom Balhara 9f1341e94e added ga4 google analytics (#5126)
* added ga4 google analytics

* added ga4 script as TODO

* get analytics app working

* Remove stale comment

* Fix types

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2022-10-22 09:39:36 +00:00
79df03de49 Profile dropdown menu will close after avatar is clicked (#5136)
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-21 20:59:14 +01:00
+9 b7892b10ef Add/Edit location dialog: "can't find the right video app? visit our app store" (#4990)
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: Matheus Benini <matheus_benini@hotmail.com>
Co-authored-by: Murilo Amaral <87545137+MuriloAmarals@users.noreply.github.com>
Co-authored-by: Rubens Rafael <70234898+RubensRafael@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: MuriloAmarals <muralha2000@gmail.com>
Co-authored-by: Matheus Muniz <matheusmuniz100@hotmail.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: C000Ldude <coolmagnas@gmail.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Eman <emmanuelgatwech@gmail.com>
2022-10-21 18:50:15 +01:00
Hariom Balharaandkodiakhq[bot] ff5fd43a9e Remove accidental use of hashedUrl for successRedirectUrl (#5153)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-21 10:09:51 +00:00
Peer Richelsen 34c13db872 pipedream app (#5119) 2022-10-21 09:49:49 +00:00
+8 e044f963f8 [CAL-216] Confirming a booking shows old toast. Also removes old notification component. (#5146)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: MuriloAmarals <muralha2000@gmail.com>
Co-authored-by: Murilo Amaral <87545137+MuriloAmarals@users.noreply.github.com>
Co-authored-by: Rubens Rafael <70234898+RubensRafael@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: gitstart <gitstart@gitstart.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: Matheus Muniz <matheusmuniz100@hotmail.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.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: C000Ldude <coolmagnas@gmail.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Eman <emmanuelgatwech@gmail.com>
2022-10-21 08:54:18 +00:00
Hariom Balhara 0344130f68 Fix unconfirmed booking marked as confirmed in DB (#5141) 2022-10-21 05:48:24 +00:00
Leo GiovanettiandAlex van Andel 5f78eceb89 Abstracting booking cancellation (#5105)
* Abstracting booking cancellation

* Tweaking to use id and not uid

* Standardizing http errors

* Moving schema to prisma zod-utils

* Update apps/web/pages/api/cancel.ts

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

* Cancel only for delete and post verbs

* Make cancellation work with uid, and id

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-20 23:28:02 +00:00
39a3a1affb allow null value for attendee phone number (#5132)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-20 18:21:27 +00:00
bc6f959c7a fix: make all apps category tab scrollable (#5113)
* feat: make left and right button workable

* refactor: separate category tab into a seperate component
to reduce unnecessary re-rendering while scrolling through the arrow buttons.

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-10-20 17:59:24 +00:00
github-actions[bot]andCrowdin Bot bb45b9617b New Crowdin translations by Github Action (#5130)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-20 12:43:13 +01:00
+8 8725034b6c Wrong css on event-types dropdown button (#4934)
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: Murilo Amaral <87545137+MuriloAmarals@users.noreply.github.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: Rubens Rafael <70234898+RubensRafael@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: MuriloAmarals <muralha2000@gmail.com>
Co-authored-by: Matheus Muniz <matheusmuniz100@hotmail.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: C000Ldude <coolmagnas@gmail.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Eman <emmanuelgatwech@gmail.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: Murilo Amaral <87545137+MuriloAmarals@users.noreply.github.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: Rubens Rafael <70234898+RubensRafael@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: MuriloAmarals <muralha2000@gmail.com>
Co-authored-by: Matheus Muniz <matheusmuniz100@hotmail.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: C000Ldude <coolmagnas@gmail.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Eman <emmanuelgatwech@gmail.com>
2022-10-20 11:35:12 +00:00
Joe Au-YeungandPeer Richelsen e53abd921e Add CTA to Meta component & button to add calendar on /settings/calendars (#5099)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-20 10:03:39 +00:00
github-actions[bot]andCrowdin Bot 7bb3397503 New Crowdin translations by Github Action (#5123)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-20 10:32:44 +01:00
github-actions[bot]andCrowdin Bot 72d19d1d62 New Crowdin translations by Github Action (#5121)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-20 10:07:43 +01:00
Nafees NazikandPeer Richelsen 2931f4f84a refactor: use the useAnimate hook (#5103)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-20 05:09:52 +00:00
Hariom BalharaandPeer Richelsen 7756b9c3a1 The links that can be directly given to embed should pre-render(Either SSG/SSR) (#4975)
* Embed SSG and consistently pass embedType query param across pages

* Embed fixes

* Code cleanup

* Add main class which tells embed which helps in identifying which area is outside the main content

* remove any special optimization handling for routing forms

* Add comments

* Small fixes

* Fix broken team booking page in embed

* Fix Fallback message dark theme

* TS Fixes

* Fixes

* Fix tests

* Remove not required code

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-19 21:25:03 +00:00
github-actions[bot]andCrowdin Bot 477941545e New Crowdin translations by Github Action (#5115)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-19 21:59:13 +01:00
zomars 237828e4ed Upgrades zod 2022-10-19 13:28:54 -06:00
e23f3b27bf New Crowdin translations by Github Action (#5107)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-19 19:01:39 +01:00
Udit TakkarandPeer Richelsen ee084aa8bd fix: add tooltip, truncate (#5096)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-19 19:00:48 +01:00
Peer Richelsen 8ece2525e5 Update README.md 2022-10-19 17:59:01 +01:00
github-actions[bot]andCrowdin Bot 865c67a962 New Crowdin translations by Github Action (#5100)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-19 17:27:37 +01:00
Leo GiovanettiandOmar López b6be94fd14 Apps Status + Updates to non-traditional calendars (#5034)
* Apps Status + Updates to non-traditional calendars

* Recurring booking tweaks

* Last tweaks

* Fixing checks

* Fixing eslint

* Reverting unneeded changes, using plain text email

* More unneeded changes revert

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

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

* Fixing lint

* Refactored appsStatus in emails

* Update packages/emails/src/templates/OrganizerScheduledEmail.tsx

Co-authored-by: Omar López <zomars@me.com>
2022-10-19 13:11:50 -03:00
Om Ray f298865d9e shows all locations and team members on /book page (#5087) 2022-10-19 09:54:18 -06:00
Kiran K 9e85770435 Fix the "Sign in with SAML" button on the login page (#5089)
* Remove the unused component

* Fix the login with SAML
2022-10-19 09:50:25 -06:00
Hariom Balhara 1427c8e792 Fix social previews title and description by doing server side translation (#5098) 2022-10-19 14:28:48 +00:00
github-actions[bot]andCrowdin Bot baa641d1f4 New Crowdin translations by Github Action (#5072)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-19 12:25:31 +01:00
Syed Ali ShahbazandAlex van Andel 51e08a7642 iCalendar timezone fix in CalDAV (#5091)
* adds support to timezone change using tzid

* code improvement with math absolute

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

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-19 11:18:08 +00:00
373c255733 Feat/3334 show times in timezones for bookingpage (#4971)
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-19 09:45:44 +00:00
sean-brydon f4fb4ddad1 Adds impersonation settings (#5088) 2022-10-19 10:33:40 +01:00
Udit TakkarandAlan 79c491986a fix: text overflow (#5080)
Co-authored-by: Alan <alannnc@gmail.com>
2022-10-18 23:31:41 +00:00
Kiran Kandzomars 759a89bb0c Reintroduce SAML SSO (#4938)
* wip reintroduce SAML SSO

* Fix the imports

* wip

* Some tweaks

* Fix the type

* Reduce the textarea height

* Cleanup

* Fix the access issues

* Make the SAML SSO active on the sidebar

* Add SP's instructions

* Remove the console.log

* Add the condition to check SAML SSO is enabled

* Replace SAML SSO with Single Sign-On

* Update to SAML feature

* Upgrade the @boxyhq/saml-jackson

* Fix the SAML part and other cleanup

* Tweaks to SAML SSO setup

* Fix the type

* Fix the import path

* Remove samlLoginUrl

* Import fixes

* Simplifies endpoints

Co-authored-by: zomars <zomars@me.com>
2022-10-18 14:34:32 -06:00
zomars 41689ecc92 Type fixes 2022-10-18 13:57:42 -06:00
9118ca328f migrate /api/book/request-reschedule to viewer.booking.requestResched… (#4488)
* migrate /api/book/request-reschedule to viewer.booking.requestReschedule in trpc

* Type fixes

Co-authored-by: hussamkhatib <hussamkhatib@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2022-10-18 13:47:36 -06:00
8fc4d342fd Hide other attendees on event types with seats (#4766)
* Add seatsHideAttendees to schema

* Add migration

* Add frontend option to hide attendees

* Pass hide attendees to email

* Hide attendee names on success email

* Add types for existing attendees

* Hide other attendees if hidden

* Pass seatsHideAttendees to Google Cal

* Add translation

* Reduce redundancy

* Fix type error

* Change toggle to show attendee information

* Minor text change

* Fix type errors

* Update snapshots

* Merge branch 'main' into seats-hide-attendees

* Add back email

* Add close.com specific types

* Add eslint ignore comments

* Merge branch 'seats-hide-attendees' of https://github.com/calcom/cal.com into seats-hide-attendees

* Simplify tests

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-18 13:41:50 -06:00
7f2560e1e3 feat: add optimistic updates for deleting and changing hidden status of event types. (#4721)
* fix: remove loading state

* feat: use optimistic update for deleting

* feat: use optimistic update for hiding

* Moved PR changes

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2022-10-18 13:34:01 -06:00
Carina Wollendorfer 3e201c9fe8 fix cancellation reason not shown (#5084) 2022-10-18 19:02:04 +00:00
Joe Au-YeungandOmar López 5ab5af753a Fix destination calendar overflow on installed and move DestinationCalendarSelector to feature package (#4778)
* Standardize destination calendar selector

* Move DestinationCalendarSelector to feature package

* Render integration name

* Add custom components to label and selected

* Render destinationCalendar on page load

* Change name to just Outlook

* Small fixes

* Merge branch 'main' into hotfix/install-calendar-overflow

* Merge branch 'main' into hotfix/install-calendar-overflow

* Fix type errors

* Fix type errors

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

* More type fixes

Co-authored-by: Omar López <zomars@me.com>
2022-10-18 18:06:26 +00:00
Jeroen Reumkens 5c01467caa #4252 Use vercel og to generate og images for meetings and apps. (#4943)
* #4252 Use vercel og to generate og images for meetings and apps.

* Removed duplication.

* Added new design for og images (wip).

* Implemented og image design for meeting image, including support for all variants.

* Implemented design for app og images.

* clenaup

* Fixed types

* Added the option to not render headseo in main shell in order to render your own.

* Added comments.

* fix

* Small tweaks.

* Fixed lock file.

* Fixed types

* Optimized svg's so vercel og supports them.

* Fixed og image on user page.

* Added truncate utils.

* Small style tweaks

* App og image alignment.

* Added og image to team/slug pages.

* Added correct variable to og image path constant.
2022-10-18 11:46:22 -06:00
Carina WollendorferandCarinaWolli 3cf47c2ee9 don't show location if only one (#5074)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-10-18 16:41:14 +02:00
8272fcd5c7 New Crowdin translations by Github Action (#5052)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-18 13:47:47 +01:00
95e3f55f26 Add checkbox that defines if SMS reminder phone number should be required (#5047)
* add phone number required checkbox

* make input required only if numberRequired is true

* fix error that smsReminderNumber is null

* code clean up

* fix typo

* save no input as null

* fix code change

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-10-18 12:47:15 +00:00
Carina WollendorferandCarinaWolli 431e4224fe fix bug with setup page (#5068)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-10-18 12:00:42 +00:00
59ef4fe459 Hotfix: Avoid theme flicker in routing form embed. (#5067)
* Hotfix: metadata parsing

* Dont remove loader as theme switching can still occur in plain sight :(

Co-authored-by: zomars <zomars@me.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2022-10-18 11:47:06 +00:00
Hariom Balharaandzomars 4211ec1a82 Fix App Redirect logic to allow EventTypeApps requiring auth to correctly redirect (#5065)
Co-authored-by: zomars <zomars@me.com>
2022-10-18 12:18:39 +01:00
Hariom Balharaandzomars 2e169801e8 Hotfix: Installed Apps Tabs Switchi g (#5063)
* Hotfix: metadata parsing

* Use Router query as tabs use shallow routing, data-fetching doesnt occur on switching tabs

Co-authored-by: zomars <zomars@me.com>
2022-10-18 08:46:06 +00:00
zomars d0d793a719 Hotfix: removed data64 avatar for 4mb payload limit 2022-10-17 14:23:00 -06:00
zomars 05f303bfad Hotfix: removed data64 avatar for 4mb payload limit 2022-10-17 13:52:04 -06:00
+9 c1b41d8251 Wrong focus ring on Help Dropdown (#5049)
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: Rubens Rafael <70234898+RubensRafael@users.noreply.github.com>
Co-authored-by: Murilo Amaral <87545137+MuriloAmarals@users.noreply.github.com>
Co-authored-by: MuriloAmarals <muralha2000@gmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: Matheus Muniz <matheusmuniz100@hotmail.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: C000Ldude <coolmagnas@gmail.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Eman <emmanuelgatwech@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
2022-10-17 19:05:00 +00:00
06732c5c6d Harsh/auto add url (#4997)
* add: auto complete for slug

* Remove useStates not required for feature

* remove: states logic

* Using touched validation from react-hook-form

* Fix size of width for steps indicador

* Apply same changes for react-hook-form touched from createNewTeam

Co-authored-by: root <root@pop-os.localdomain>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
2022-10-17 18:52:24 +00:00
zomars aa038acafd Hotfix: metadata parsing 2022-10-17 12:18:59 -06:00
402 changed files with 8171 additions and 6177 deletions
@@ -5,7 +5,7 @@ name: 'Chromatic'
# Event for the workflow
on:
pull_request_target: # So we can test on forks
pull_request: # So we can test on forks
branches:
- main
paths:
+1 -1
View File
@@ -1,6 +1,6 @@
name: Check types
on:
pull_request_target:
pull_request:
branches:
- main
paths:
+1 -1
View File
@@ -2,7 +2,7 @@ name: E2E App-Store Apps
on:
push:
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
pull_request_target: # So we can test on forks
pull_request: # So we can test on forks
branches:
- main
paths-ignore:
+1 -1
View File
@@ -1,6 +1,6 @@
name: E2E Embed tests and booking flow(for non-embed as well)
on:
pull_request_target: # So we can test on forks
pull_request: # So we can test on forks
branches:
- main
# Embed e2e - tests verify booking flow which is applicable to non-embed case also. So, don't ignore apps/web changes.
+1 -1
View File
@@ -2,7 +2,7 @@ name: E2E test
on:
push:
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
pull_request_target: # So we can test on forks
pull_request: # So we can test on forks
branches:
- main
paths:
+1 -1
View File
@@ -1,6 +1,6 @@
name: "Pull Request Labeler"
on:
- pull_request_target
- pull_request
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
+1 -1
View File
@@ -1,6 +1,6 @@
name: Lint
on:
pull_request_target:
pull_request:
branches:
- main
paths:
+1 -1
View File
@@ -2,7 +2,7 @@ name: Unit tests
on:
push:
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
pull_request_target: # So we can test on forks
pull_request: # So we can test on forks
branches:
- main
paths:
+2 -2
View File
@@ -37,7 +37,7 @@
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
<a href="https://github.com/orgs/calcom/projects/1/views/25"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
<a href="https://github.com/orgs/calcom/projects/9"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
@@ -324,7 +324,7 @@ We have a list of [help wanted](https://github.com/orgs/calcom/projects/1/views/
6. In the third page (Test Users), add the Google account(s) you'll using. Make sure the details are correct on the last page of the wizard and your consent screen will be configured.
7. Now select [Credentials](https://console.cloud.google.com/apis/credentials) from the side pane and then select Create Credentials. Select the OAuth Client ID option.
8. Select Web Application as the Application Type.
9. Under Authorized redirect URI's, select Add URI and then add the URI `<Cal.com URL>/api/integrations/googlecalendar/callback` replacing Cal.com URL with the URI at which your application runs.
9. Under Authorized redirect URI's, select Add URI and then add the URI `<Cal.com URL>/api/integrations/googlecalendar/callback` and `<Cal.com URL>/api/auth/callback/google` replacing Cal.com URL with the URI at which your application runs.
10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs.
11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the .env file as the value for GOOGLE_API_CREDENTIALS key.
@@ -0,0 +1,20 @@
import { ComponentMeta } from "@storybook/react";
import { ToggleGroup } from "@calcom/ui/v2/core/form/ToggleGroup";
export default {
title: "Toggle Group",
component: ToggleGroup,
} as ComponentMeta<typeof ToggleGroup>;
export const Default = () => {
return (
<ToggleGroup
defaultValue="12"
options={[
{ value: "12", label: "12h" },
{ value: "24", label: "24h" },
]}
/>
);
};
+1 -1
View File
@@ -6,13 +6,13 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton } from "@calcom/app-store/components";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { App as AppType } from "@calcom/types/App";
import { Button, SkeletonButton } from "@calcom/ui";
import { Icon } from "@calcom/ui/Icon";
import Shell from "@calcom/ui/Shell";
import Badge from "@calcom/ui/v2/core/Badge";
import showToast from "@calcom/ui/v2/core/notifications";
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
@@ -23,11 +23,11 @@ const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App
type={data.type}
render={(installProps) => {
return (
<Button {...installProps} className="w-full" color="minimal">
<Button {...installProps} className="flex w-full align-top" color="minimal">
{data.image && (
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
)}
<p>{`${t("add")} ${data.label}`}</p>
<p className="text-left">{`${t("add")} ${data.label}`}</p>
</Button>
);
}}
@@ -64,10 +64,12 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
<Select
name="additionalCalendar"
placeholder={
<Button className="rounded-md" StartIcon={Icon.FiPlus} color="secondary">
{t("add")}
</Button>
<div className="flex justify-start text-black">
<Icon.FiPlus className="-ml-1 h-4 w-4 ltr:mr-2 rtl:ml-2 rtl:-mr-1" />
<p>{t("add")}</p>
</div>
}
className="min-w-44"
options={options}
isSearchable={false}
isLoading={isLoading}
-19
View File
@@ -1,19 +0,0 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import AppCard from "./AppCard";
export default function AllApps({ apps }: { apps: App[] }) {
const { t } = useLocale();
return (
<div className="mb-16">
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("all_apps")}</h2>
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
{apps.map((app) => (
<AppCard key={app.name} app={app} />
))}
</div>
</div>
);
}
@@ -14,6 +14,8 @@ import { showToast, SkeletonText } from "@calcom/ui/v2";
import { Button, SkeletonButton, Shell } from "@calcom/ui/v2";
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
import HeadSeo from "@components/seo/head-seo";
const Component = ({
name,
type,
@@ -66,7 +68,7 @@ const Component = ({
return (
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
{hasImages && (
<div className="flex-2 mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:rounded-md">
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
{images ? (
images.map((img) => (
<img
@@ -83,7 +85,7 @@ const Component = ({
)}
<div
className={classNames(
"sticky top-0 -mt-4 max-w-xl flex-1 pb-12 text-sm lg:pb-0",
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
hasImages && "lg:ml-8"
)}>
<div className="mb-8 flex pt-4">
@@ -276,6 +278,7 @@ const Component = ({
export default function App(props: {
name: string;
description: AppType["description"];
type: AppType["type"];
isGlobal?: AppType["isGlobal"];
logo: string;
@@ -300,7 +303,16 @@ export default function App(props: {
const { t } = useLocale();
return (
<Shell large isPublic heading={t("app_store")} backPath="/apps">
<Shell large isPublic heading={t("app_store")} backPath="/apps" withoutSeo>
<HeadSeo
title={props.name}
description={props.description}
app={{ slug: props.logo, name: props.name, description: props.description }}
nextSeoProps={{
nofollow: true,
noindex: true,
}}
/>
{props.licenseRequired ? (
<LicenseRequired>
<Component {...props} />
-44
View File
@@ -1,44 +0,0 @@
import Link from "next/link";
import { trpc } from "@calcom/trpc/react";
import { App } from "@calcom/types/App";
import Badge from "@calcom/ui/Badge";
import Button from "@calcom/ui/Button";
interface AppCardProps {
app: App;
}
export default function AppCard(props: AppCardProps) {
const { data: user } = trpc.useQuery(["viewer.me"]);
return (
<Link href={"/apps/" + props.app.slug}>
<a
className="block h-full rounded-sm border border-gray-300 p-5 hover:bg-neutral-50"
data-testid={`app-store-app-card-${props.app.slug}`}>
<div className="flex">
{
// eslint-disable-next-line @next/next/no-img-element
<img src={props.app.logo} alt={props.app.name + " Logo"} className="mb-4 h-12 w-12 rounded-sm" />
}
<Button
color="secondary"
className="ml-auto flex self-start"
onClick={() => {
console.log("The magic is supposed to happen here");
}}>
Add
</Button>
</div>
<div className="flex items-center">
<h3 className="font-medium">{props.app.name}</h3>
</div>
{/* TODO: add reviews <div className="flex text-sm text-gray-800">
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
</div> */}
<p className="mt-2 truncate text-sm text-gray-500">{props.app.description}</p>
</a>
</Link>
);
}
@@ -3,8 +3,8 @@ import Link from "next/link";
import { Fragment } from "react";
import { InstallAppButton } from "@calcom/app-store/components";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui/Icon";
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
@@ -12,14 +12,14 @@ import { Alert, Button, EmptyScreen } from "@calcom/ui/v2";
import { List } from "@calcom/ui/v2/core/List";
import { ShellSubHeading } from "@calcom/ui/v2/core/Shell";
import Switch from "@calcom/ui/v2/core/Switch";
import showToast from "@calcom/ui/v2/core/notifications";
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
import { QueryCell } from "@lib/QueryCell";
import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector";
import IntegrationListItem from "@components/apps/IntegrationListItem";
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
import AdditionalCalendarSelector from "@components/v2/apps/AdditionalCalendarSelector";
import DestinationCalendarSelector from "@components/v2/apps/DestinationCalendarSelector";
import IntegrationListItem from "@components/v2/apps/IntegrationListItem";
type Props = {
onChanged: () => unknown | Promise<unknown>;
@@ -287,7 +287,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
<h1 className="text-sm font-semibold">{t("create_events_on")}</h1>
<p className="text-sm font-normal">{t("set_calendar")}</p>
</div>
<div className="flex justify-end md:w-6/12">
<div className="justify-end md:w-6/12">
<DestinationCalendarSelector
onChange={mutation.mutate}
hidePlaceholder
-43
View File
@@ -1,43 +0,0 @@
import Image from "next/image";
import Link from "next/link";
import { useLocale } from "@calcom/lib/hooks/useLocale";
export default function AppStoreCategories({
categories,
}: {
categories: {
name: string;
count: number;
}[];
}) {
const { t } = useLocale();
return (
<div className="mb-16">
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("popular_categories")}</h2>
<div className="grid-col-1 grid w-full gap-3 overflow-scroll sm:grid-flow-col">
{categories.map((category) => (
<Link key={category.name} href={"/apps/categories/" + category.name}>
<a
data-testid={`app-store-category-${category.name}`}
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
<div className="min-w-24 -ml-5 text-center sm:ml-0">
<Image
alt={category.name}
width="352"
height="252"
layout="responsive"
src={"/app-store/" + category.name + ".svg"}
/>
</div>
<div className="self-center">
<h3 className="font-medium capitalize">{category.name}</h3>
<p className="text-sm text-gray-500">{category.count} apps</p>
</div>
</a>
</Link>
))}
</div>
</div>
);
}
@@ -2,6 +2,8 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { ReactNode, useEffect, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui";
import { ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/core/List";
import classNames from "@lib/classNames";
@@ -17,7 +19,9 @@ function IntegrationListItem(props: {
logo: string;
destination?: boolean;
separate?: boolean;
invalidCredential?: boolean;
}): JSX.Element {
const { t } = useLocale();
const router = useRouter();
const { hl } = router.query;
const [highlight, setHighlight] = useState(hl === props.slug);
@@ -44,6 +48,15 @@ function IntegrationListItem(props: {
<Link href={"/apps/" + props.slug}>{props.name || title}</Link>
</ListItemTitle>
<ListItemText component="p">{props.description}</ListItemText>
{/* Alert error that key stopped working. */}
{props.invalidCredential && (
<div className="flex items-center space-x-2">
<Icon.FiAlertCircle className="w-8 text-red-500 sm:w-4" />
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
{t("invalid_credential")}
</ListItemText>
</div>
)}
</div>
<div>{props.actions}</div>
</div>
-79
View File
@@ -1,79 +0,0 @@
import Glide, { Options } from "@glidejs/glide";
import "@glidejs/glide/dist/css/glide.core.min.css";
import "@glidejs/glide/dist/css/glide.theme.min.css";
import { useEffect, useRef } from "react";
import { Icon } from "@calcom/ui/Icon";
const Slider = <T extends string | unknown>({
title = "",
className = "",
items,
itemKey = (item) => `${item}`,
renderItem,
options = {},
}: {
title?: string;
className?: string;
items: T[];
itemKey?: (item: T) => string;
renderItem?: (item: T) => JSX.Element;
options?: Options;
}) => {
const glide = useRef(null);
const slider = useRef<Glide.Properties | null>(null);
useEffect(() => {
if (glide.current) {
slider.current = new Glide(glide.current, {
type: "carousel",
...options,
}).mount();
}
return () => slider.current?.destroy();
}, [options]);
return (
<div className={`mb-2 ${className}`}>
<style jsx global>
{`
.glide__slide {
height: auto !important;
}
`}
</style>
<div className="glide" ref={glide}>
<div className="flex cursor-default">
{title && (
<div>
<h2 className="mt-0 mb-2 text-lg font-semibold text-gray-900">{title}</h2>
</div>
)}
<div className="glide__arrows ml-auto" data-glide-el="controls">
<button data-glide-dir="<" className="mr-4">
<Icon.FiArrowLeft className="h-5 w-5 text-gray-600 hover:text-black" />
</button>
<button data-glide-dir=">">
<Icon.FiArrowRight className="h-5 w-5 text-gray-600 hover:text-black" />
</button>
</div>
</div>
<div className="glide__track" data-glide-el="track">
<ul className="glide__slides">
{items.map((item) => {
if (typeof renderItem !== "function") return null;
return (
<li key={itemKey(item)} className="glide__slide h-auto pl-0">
{renderItem(item)}
</li>
);
})}
</ul>
</div>
</div>
</div>
);
};
export default Slider;
@@ -1,30 +0,0 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import AppCard from "./AppCard";
import Slider from "./Slider";
const TrendingAppsSlider = <T extends App>({ items }: { items: T[] }) => {
const { t } = useLocale();
return (
<Slider<T>
className="mb-16"
title={t("trending_apps")}
items={items.filter((app) => !!app.trending)}
itemKey={(app) => app.name}
options={{
perView: 3,
breakpoints: {
768 /* and below */: {
perView: 1,
},
},
}}
renderItem={(app) => <AppCard app={app} />}
/>
);
};
/** @deprecated use v2 instead, located at packages/ui/v2/core/apps */
export default TrendingAppsSlider;
-62
View File
@@ -1,62 +0,0 @@
import { signIn } from "next-auth/react";
import { Dispatch, SetStateAction } from "react";
import { useFormContext } from "react-hook-form";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
interface Props {
email: string;
samlTenantID: string;
samlProductID: string;
hostedCal: boolean;
setErrorMessage: Dispatch<SetStateAction<string | null>>;
}
export default function SAMLLogin(props: Props) {
const { t } = useLocale();
const methods = useFormContext();
const telemetry = useTelemetry();
const mutation = trpc.useMutation("viewer.public.samlTenantProduct", {
onSuccess: async (data) => {
await signIn("saml", {}, { tenant: data.tenant, product: data.product });
},
onError: (err) => {
props.setErrorMessage(err.message);
},
});
return (
<div className="mt-5">
<Button
color="secondary"
data-testid="saml"
className="flex w-full justify-center"
onClick={async (event) => {
event.preventDefault();
// track Google logins. Without personal data/payload
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
if (!props.hostedCal) {
await signIn("saml", {}, { tenant: props.samlTenantID, product: props.samlProductID });
} else {
if (props.email.length === 0) {
props.setErrorMessage(t("saml_email_required"));
return;
}
// hosted solution, fetch tenant and product from the backend
mutation.mutate({
email: methods.getValues("email"),
});
}
}}>
{t("signin_with_saml")}
</Button>
</div>
);
}
@@ -2,12 +2,12 @@ import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
import { Form } from "@calcom/ui/form/fields";
import showToast from "@calcom/ui/v2/core/notifications";
import { HttpError } from "@lib/core/http/error";
@@ -1,33 +1,41 @@
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
import Tooltip from "@calcom/ui/v2/core/Tooltip";
import { Props } from "./pages/AvailabilityPage";
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
return locations.length ? (
<div>
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
{locations.map((location) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
// It's possible that the location app got uninstalled
return null;
}
return (
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
<img
src={eventLocationType.iconUrl}
className={classNames(
"mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 ",
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
)}
alt={`${eventLocationType.label} icon`}
/>
<span key={location.type}>{locationKeyToString(location)} </span>
</div>
);
})}
</div>
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
{locations.map((location) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
// It's possible that the location app got uninstalled
return null;
}
return (
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
<img
src={eventLocationType.iconUrl}
className={classNames(
"mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 ",
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
)}
alt={`${eventLocationType.label} icon`}
/>
<Tooltip content={locationKeyToString(location)}>
<a
target="_blank"
href={locationKeyToString(location) ?? "/"}
className="truncate"
key={location.type}
rel="noreferrer">
{locationKeyToString(location)}
</a>
</Tooltip>
</div>
);
})}
</div>
) : (
<></>
+25 -10
View File
@@ -4,15 +4,18 @@ import { FC, useEffect, useState } from "react";
import dayjs, { Dayjs } from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { nameOfDay } from "@calcom/lib/weekday";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
import { SkeletonContainer, SkeletonText } from "@calcom/ui";
import { ToggleGroup } from "@calcom/ui/v2/core/form/ToggleGroup";
import classNames from "@lib/classNames";
import { timeZone } from "@lib/clock";
type AvailableTimesProps = {
timeFormat: string;
timeFormat: TimeFormat;
onTimeFormatChange: (is24Hour: boolean) => void;
eventTypeId: number;
recurringCount: number | undefined;
eventTypeSlug: string;
@@ -31,6 +34,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
eventTypeSlug,
recurringCount,
timeFormat,
onTimeFormatChange,
seatsPerTimeSlot,
ethSignature,
}) => {
@@ -48,14 +52,25 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
return (
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
<div className="mb-4 text-left text-base">
<span className="text-bookingdarker dark:text-darkgray-800 mb-8 w-1/2 break-words font-semibold text-gray-900">
{nameOfDay(i18n.language, Number(date.format("d")))}
</span>
<span className="text-bookinglight font-medium">
{date.format(", D ")}
{date.toDate().toLocaleString(i18n.language, { month: "long" })}
</span>
<div className="mb-6 flex items-center text-left text-base">
<div className="mr-4">
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span>
<span className="text-bookinglight font-medium">
, {date.toDate().toLocaleString(i18n.language, { month: "long" })} {date.format(" D ")}
</span>
</div>
<div className="ml-auto">
<ToggleGroup
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
options={[
{ value: "12", label: t("12_hour_short") },
{ value: "24", label: t("24_hour_short") },
]}
/>
</div>
</div>
<div className="-mb-5 grid flex-grow grid-cols-1 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
{slots.length > 0 &&
@@ -65,7 +80,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
query: Record<string, string | number | string[] | undefined>;
};
const bookingUrl: BookingURL = {
pathname: "book",
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
query: {
...router.query,
date: dayjs(slot.time).format(),
@@ -70,7 +70,9 @@ const BookingDescription: FC<Props> = (props) => {
)}
/>
</div>
<EventTypeDescriptionSafeHTML eventType={eventType} />
<div className="max-w-[calc(100%_-_2rem)] flex-shrink break-words">
<EventTypeDescriptionSafeHTML eventType={eventType} />
</div>
</div>
)}
{eventType?.requiresConfirmation && (
@@ -85,11 +87,9 @@ const BookingDescription: FC<Props> = (props) => {
{t("requires_confirmation")}
</div>
)}
{!isBookingPage && !props.rescheduleUid ? (
<AvailableEventLocations
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
/>
) : null}
<AvailableEventLocations
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
/>
<p
className={classNames(
"text-sm font-medium",
+53 -56
View File
@@ -1,24 +1,26 @@
import { BookingStatus } from "@prisma/client";
import { useRouter } from "next/router";
import { useState } from "react";
import { useState, useMemo } from "react";
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { formatTime } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { inferQueryInput, inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
import { Tooltip } from "@calcom/ui/Tooltip";
import { TextArea } from "@calcom/ui/form/fields";
import Badge from "@calcom/ui/v2/core/Badge";
import Button from "@calcom/ui/v2/core/Button";
import MeetingTimeInTimezones from "@calcom/ui/v2/core/MeetingTimeInTimezones";
import Tooltip from "@calcom/ui/v2/core/Tooltip";
import showToast from "@calcom/ui/v2/core/notifications";
import useMeQuery from "@lib/hooks/useMeQuery";
import { extractRecurringDates } from "@lib/parseDate";
// import { extractRecurringDates } from "@lib/parseDate";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import TableActions, { ActionType } from "@components/ui/TableActions";
@@ -174,18 +176,20 @@ function BookingListItem(booking: BookingItemProps) {
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
};
// Extract recurring dates is intensive to run, so use useMemo.
// Calculate the booking date(s) and setup recurring event data to show
const recurringStrings: string[] = [];
const recurringDates: Date[] = [];
// @FIXME: This is importing the RRULE library which is already heavy. Find out a more optimal way do this.
// if (booking.recurringBookings !== undefined && booking.eventType.recurringEvent?.freq !== undefined) {
// [recurringStrings, recurringDates] = extractRecurringDates(booking, user?.timeZone, i18n);
// }
const [recurringStrings, recurringDates] = useMemo(() => {
if (booking.recurringBookings !== undefined && booking.eventType.recurringEvent?.freq !== undefined) {
return extractRecurringDates(booking, user?.timeZone, i18n);
}
return [[], []];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user?.timeZone, i18n.language, booking.recurringBookings]);
const location = booking.location || "";
const onClick = () => {
const onClickTableData = () => {
router.push({
pathname: "/success",
query: {
@@ -207,8 +211,6 @@ function BookingListItem(booking: BookingItemProps) {
});
};
const utcOffset = dayjs().tz(user?.timeZone).utcOffset();
return (
<>
<RescheduleDialog
@@ -257,19 +259,21 @@ function BookingListItem(booking: BookingItemProps) {
</Dialog>
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
<td className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[10rem]" onClick={onClick}>
<td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}>
<div className="cursor-pointer py-4">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="text-sm text-gray-500">
{dayjs
.utc(booking.startTime)
.utcOffset(utcOffset)
.format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}{" "}
-{" "}
{dayjs
.utc(booking.endTime)
.utcOffset(utcOffset)
.format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
<MeetingTimeInTimezones
timeFormat={user?.timeFormat}
userTimezone={user?.timeZone}
startTime={booking.startTime}
endTime={booking.endTime}
attendees={booking.attendees}
/>
</div>
{isPending && (
@@ -290,29 +294,25 @@ function BookingListItem(booking: BookingItemProps) {
)}
<div className="mt-2 text-sm text-gray-400">
<RecurringBookingsTooltip
booking={booking}
recurringStrings={recurringStrings}
recurringDates={recurringDates}
/>
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
</div>
</div>
</td>
<td className={"w-full px-4" + (isRejected ? " line-through" : "")} onClick={onClick}>
<td className={"w-full px-4" + (isRejected ? " line-through" : "")} onClick={onClickTableData}>
{/* Time and Badges for mobile */}
<div className="w-full pt-4 pb-2 sm:hidden">
<div className="flex w-full items-center justify-between sm:hidden">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="pr-2 text-sm text-gray-500">
{dayjs
.utc(booking.startTime)
.utcOffset(utcOffset)
.format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}{" "}
-{" "}
{dayjs
.utc(booking.endTime)
.utcOffset(utcOffset)
.format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
<MeetingTimeInTimezones
timeFormat={user?.timeFormat}
userTimezone={user?.timeZone}
startTime={booking.startTime}
endTime={booking.endTime}
attendees={booking.attendees}
/>
</div>
</div>
@@ -332,11 +332,7 @@ function BookingListItem(booking: BookingItemProps) {
</Badge>
)}
<div className="text-sm text-gray-400 sm:hidden">
<RecurringBookingsTooltip
booking={booking}
recurringStrings={recurringStrings}
recurringDates={recurringDates}
/>
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
</div>
</div>
@@ -356,7 +352,7 @@ function BookingListItem(booking: BookingItemProps) {
</div>
{booking.description && (
<div
className="max-w-52 md:max-w-96 truncate text-sm text-gray-600"
className="max-w-10/12 sm:max-w-40 md:max-w-56 xl:max-w-80 lg:max-w-64 truncate text-sm text-gray-600"
title={booking.description}>
&quot;{booking.description}&quot;
</div>
@@ -397,17 +393,18 @@ function BookingListItem(booking: BookingItemProps) {
interface RecurringBookingsTooltipProps {
booking: BookingItemProps;
recurringStrings: string[];
recurringDates: Date[];
}
const RecurringBookingsTooltip = ({
booking,
recurringStrings,
recurringDates,
}: RecurringBookingsTooltipProps) => {
const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookingsTooltipProps) => {
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const user = query.data;
const { t } = useLocale();
const now = new Date();
const recurringCount = recurringDates.filter((date) => {
return date >= now;
}).length;
return (
(booking.recurringBookings &&
@@ -418,9 +415,11 @@ const RecurringBookingsTooltip = ({
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
<div className="flex">
<Tooltip
content={recurringStrings.map((aDate, key) => (
content={recurringDates.map((aDate, key) => (
<p key={key} className={classNames(recurringDates[key] < now && "line-through")}>
{aDate}
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)}
{" - "}
{dayjs(aDate).format("D MMMM YYYY")}
</p>
))}>
<div className="text-gray-600 dark:text-white">
@@ -431,14 +430,12 @@ const RecurringBookingsTooltip = ({
<p className="mt-1 pl-5 text-xs">
{booking.status === BookingStatus.ACCEPTED
? `${t("event_remaining", {
count: recurringDates.length,
count: recurringCount,
})}`
: getEveryFreqFor({
t,
recurringEvent: booking.eventType.recurringEvent,
recurringCount: recurringDates.filter((date) => {
return date >= now;
}).length,
recurringCount,
})}
</p>
</div>
@@ -12,6 +12,7 @@ type Props = {
booking: {
title?: string;
uid?: string;
id?: number;
};
profile: {
name: string | null;
@@ -57,13 +58,13 @@ export default function CancelBooking(props: Props) {
className="mt-2 mb-3 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white sm:mb-3 "
rows={3}
/>
<div className=" flex flex-col-reverse rtl:space-x-reverse sm:flex-row">
<div className="flex flex-col-reverse rtl:space-x-reverse sm:flex-row">
{!props.recurringEvent && (
<div className=" border-bookinglightest mt-5 flex w-full justify-center border-t pt-3 sm:mt-0 sm:justify-start sm:border-0 sm:pt-0">
<div className="border-bookinglightest mt-5 flex w-full justify-center border-t pt-3 sm:mt-0 sm:justify-start sm:border-0 sm:pt-0">
<Button
color="secondary"
className="border-0 sm:border"
onClick={() => router.push("/reschedule/" + booking?.uid)}>
onClick={() => router.push(`/reschedule/${booking?.uid}`)}>
{t("reschedule_this")}
</Button>
</div>
@@ -79,8 +80,8 @@ export default function CancelBooking(props: Props) {
setLoading(true);
const payload = {
uid: booking?.uid,
reason: cancellationReason,
id: booking?.id,
cancellationReason: cancellationReason,
};
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
+2 -26
View File
@@ -1,25 +1,19 @@
import { FC, useEffect, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Switch } from "@calcom/ui/v2";
import TimezoneSelect, { ITimezoneOption } from "@calcom/ui/v2/core/TimezoneSelect";
import { is24h, timeZone } from "../../lib/clock";
import { timeZone } from "../../lib/clock";
type Props = {
onSelectTimeZone: (selectedTimeZone: string) => void;
onToggle24hClock: (is24hClock: boolean) => void;
timeFormat: string;
hideTimeFormatToggle?: boolean;
};
const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone, timeFormat, hideTimeFormatToggle }) => {
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
const [selectedTimeZone, setSelectedTimeZone] = useState("");
const [is24hClock, setIs24hClock] = useState(timeFormat === "HH:mm" && true);
const { t } = useLocale();
useEffect(() => {
setIs24hClock(is24h());
setSelectedTimeZone(timeZone());
}, []);
@@ -29,28 +23,10 @@ const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone, timeFormat
}
}, [selectedTimeZone, onSelectTimeZone]);
const handle24hClockToggle = (is24hClock: boolean) => {
setIs24hClock(is24hClock);
onToggle24hClock(is24h(is24hClock));
};
return selectedTimeZone !== "" ? (
<div className="dark:border-darkgray-300 dark:bg-darkgray-200 rounded-sm border border-gray-200 bg-white px-4 pt-4 pb-3 shadow-sm">
<div className="mb-4 flex">
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
{!hideTimeFormatToggle && (
<div className="ml-auto flex items-center">
<label className="ltl:mr-3 mr-2 align-text-top text-sm font-medium text-neutral-700 ltr:ml-3 rtl:mr-3 dark:text-white">
{t("am_pm")}
</label>
<Switch
name="24hClock"
label={t("24_h")}
defaultChecked={is24hClock}
onCheckedChange={handle24hClockToggle}
/>
</div>
)}
</div>
<TimezoneSelect
id="timeZone"
@@ -1,10 +1,9 @@
// Get router variables
import autoAnimate from "@formkit/auto-animate";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { EventType } from "@prisma/client";
import * as Popover from "@radix-ui/react-popover";
import { TFunction } from "next-i18next";
import { useRouter } from "next/router";
import { useReducer, useEffect, useMemo, useState, useRef } from "react";
import { useReducer, useEffect, useMemo, useState } from "react";
import { Toaster } from "react-hot-toast";
import { FormattedNumber, IntlProvider } from "react-intl";
import { z } from "zod";
@@ -27,7 +26,7 @@ import useTheme from "@calcom/lib/hooks/useTheme";
import notEmpty from "@calcom/lib/notEmpty";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { detectBrowserTimeFormat, getIs24hClockFromLocalStorage } from "@calcom/lib/timeFormat";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui/Icon";
import DatePicker from "@calcom/ui/v2/modules/booker/DatePicker";
@@ -48,6 +47,7 @@ import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
// Get router variables
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
const router = useRouter();
const path = router.asPath.split("/");
@@ -108,6 +108,7 @@ const useSlots = ({
const SlotPicker = ({
eventType,
timeFormat,
onTimeFormatChange,
timeZone,
recurringEventCount,
users,
@@ -116,7 +117,8 @@ const SlotPicker = ({
ethSignature,
}: {
eventType: Pick<EventType, "id" | "schedulingType" | "slug">;
timeFormat: string;
timeFormat: TimeFormat;
onTimeFormatChange: (is24Hour: boolean) => void;
timeZone?: string;
seatsPerTimeSlot?: number;
recurringEventCount?: number;
@@ -129,11 +131,8 @@ const SlotPicker = ({
const { date, setQuery: setDate } = useRouterQuery("date");
const { month, setQuery: setMonth } = useRouterQuery("month");
const router = useRouter();
const slotPickerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
slotPickerRef.current && autoAnimate(slotPickerRef.current);
}, [slotPickerRef]);
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
useEffect(() => {
if (!router.isReady) return;
@@ -203,6 +202,7 @@ const SlotPicker = ({
slots={selectedDate && slots[selectedDate.format("YYYY-MM-DD")]}
date={selectedDate}
timeFormat={timeFormat}
onTimeFormatChange={onTimeFormatChange}
eventTypeId={eventType.id}
eventTypeSlug={eventType.slug}
seatsPerTimeSlot={seatsPerTimeSlot}
@@ -215,35 +215,20 @@ const SlotPicker = ({
};
function TimezoneDropdown({
onChangeTimeFormat,
onChangeTimeZone,
timeZone,
timeFormat,
hideTimeFormatToggle,
}: {
onChangeTimeFormat: (newTimeFormat: string) => void;
onChangeTimeZone: (newTimeZone: string) => void;
timeZone?: string;
timeFormat: string;
hideTimeFormatToggle?: boolean;
}) {
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
useEffect(() => {
handleToggle24hClock(!!getIs24hClockFromLocalStorage());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleSelectTimeZone = (newTimeZone: string) => {
onChangeTimeZone(newTimeZone);
localStorageTimeZone(newTimeZone);
setIsTimeOptionsOpen(false);
};
const handleToggle24hClock = (is24hClock: boolean) => {
onChangeTimeFormat(is24hClock ? "HH:mm" : "h:mma");
};
return (
<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">
@@ -262,12 +247,7 @@ function TimezoneDropdown({
hideWhenDetached
align="start"
className="animate-fade-in-up absolute left-0 top-2 w-80 max-w-[calc(100vw_-_1.5rem)]">
<TimeOptions
onSelectTimeZone={handleSelectTimeZone}
onToggle24hClock={handleToggle24hClock}
timeFormat={timeFormat}
hideTimeFormatToggle={hideTimeFormatToggle}
/>
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
@@ -299,16 +279,9 @@ const useRouterQuery = <T extends string>(name: T) => {
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
const timeFormatTotimeFormatString = (timeFormat?: number | null) => {
if (!timeFormat) return null;
return timeFormat === 24 ? "HH:mm" : "h:mma";
};
const AvailabilityPage = ({ profile, eventType }: Props) => {
const { data: user } = trpc.useQuery(["viewer.me"]);
const timeFormatFromProfile = timeFormatTotimeFormatString(user?.timeFormat);
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
const router = useRouter();
const isEmbed = useIsEmbed();
const isEmbed = useIsEmbed(restProps.isEmbed);
const query = dateQuerySchema.parse(router.query);
const { rescheduleUid } = query;
useTheme(profile.theme);
@@ -319,7 +292,12 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
const isBackgroundTransparent = useIsBackgroundTransparent();
const [timeZone, setTimeZone] = useState<string>();
const [timeFormat, setTimeFormat] = useState<string>("HH:mm");
const [timeFormat, setTimeFormat] = useState<TimeFormat>(detectBrowserTimeFormat);
const onTimeFormatChange = (is24Hours: boolean) => {
setTimeFormat(is24Hours ? TimeFormat.TWENTY_FOUR_HOUR : TimeFormat.TWELVE_HOUR);
setIs24hClockInLocalStorage(is24Hours);
};
const [gateState, gateDispatcher] = useReducer(
(state: GateState, newState: Partial<GateState>) => ({
@@ -331,8 +309,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
useEffect(() => {
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
setTimeFormat(timeFormatFromProfile || detectBrowserTimeFormat);
}, [timeFormatFromProfile]);
}, []);
// TODO: Improve this;
useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO");
@@ -354,24 +331,13 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
const timezoneDropdown = useMemo(
() => (
<TimezoneDropdown
timeFormat={timeFormat}
onChangeTimeFormat={setTimeFormat}
timeZone={timeZone}
onChangeTimeZone={setTimeZone}
// Currently we don't allow the user to change the timeformat when they're logged in,
// the only way to change it is if they go to their profile.
hideTimeFormatToggle={!!timeFormatFromProfile}
/>
),
[timeZone, timeFormat, timeFormatFromProfile]
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
[timeZone]
);
const stripeAppData = getStripeAppData(eventType);
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
const rawSlug = profile.slug ? profile.slug.split("/") : [];
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
const slug = rawSlug.join("/");
// Define conditional gates here
const gates = [
@@ -386,8 +352,16 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
<HeadSeo
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
name={profile.name || undefined}
username={slug || undefined}
meeting={{
title: eventType.title,
profile: { name: `${profile.name}`, image: profile.image },
users: [
...(eventType.users || []).map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
],
}}
nextSeoProps={{
nofollow: eventType.hidden,
noindex: eventType.hidden,
@@ -416,7 +390,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
<div
className={classNames(
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
"min-w-full md:w-[280px] md:min-w-[280px]",
"min-w-full md:w-[230px] md:min-w-[230px]",
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
)}>
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
@@ -496,6 +470,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
}
eventType={eventType}
timeFormat={timeFormat}
onTimeFormatChange={onTimeFormatChange}
timeZone={timeZone}
users={userList}
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
@@ -1,6 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { EventTypeCustomInputType, WorkflowActions } from "@prisma/client";
import { SchedulingType } from "@prisma/client";
import { useMutation } from "@tanstack/react-query";
import { isValidPhoneNumber } from "libphonenumber-js";
import { useSession } from "next-auth/react";
@@ -32,6 +31,7 @@ import {
} from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import { formatTime } from "@calcom/lib/date-fns";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
@@ -47,6 +47,7 @@ import { Button } from "@calcom/ui/v2";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
import { ensureArray } from "@lib/ensureArray";
import useMeQuery from "@lib/hooks/useMeQuery";
import createBooking from "@lib/mutations/bookings/create-booking";
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
import { parseDate, parseRecurringDates } from "@lib/parseDate";
@@ -84,9 +85,13 @@ const BookingPage = ({
recurringEventCount,
hasHashedBookingLink,
hashedLink,
...restProps
}: BookingPageProps) => {
const { t, i18n } = useLocale();
const isEmbed = useIsEmbed();
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const user = query.data;
const isEmbed = useIsEmbed(restProps.isEmbed);
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const router = useRouter();
@@ -115,14 +120,14 @@ const BookingPage = ({
const mutation = useMutation(createBooking, {
onSuccess: async (responseData) => {
const { id, attendees, paymentUid } = responseData;
const { id, paymentUid } = responseData;
if (paymentUid) {
return await router.push(
createPaymentLink({
paymentUid,
date,
name: attendees[0].name,
email: attendees[0].email,
name: bookingForm.getValues("name"),
email: bookingForm.getValues("email"),
absolute: false,
})
);
@@ -136,8 +141,8 @@ const BookingPage = ({
eventSlug: eventType.slug,
username: profile.slug,
reschedule: !!rescheduleUid,
name: attendees[0].name,
email: attendees[0].email,
name: bookingForm.getValues("name"),
email: bookingForm.getValues("email"),
location: responseData.location,
eventName: profile.eventName || "",
bookingId: id,
@@ -262,11 +267,13 @@ const BookingPage = ({
phone: z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional(),
.optional()
.nullable(),
smsReminderNumber: z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional(),
.optional()
.nullable(),
})
.passthrough();
@@ -358,7 +365,9 @@ const BookingPage = ({
hasHashedBookingLink,
hashedLink,
smsReminderNumber:
selectedLocationType === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
selectedLocationType === LocationType.Phone
? booking.phone
: booking.smsReminderNumber || undefined,
ethSignature: gateState.rainbowToken,
}));
recurringMutation.mutate(recurringBookings);
@@ -386,7 +395,9 @@ const BookingPage = ({
hasHashedBookingLink,
hashedLink,
smsReminderNumber:
selectedLocationType === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
selectedLocationType === LocationType.Phone
? booking.phone
: booking.smsReminderNumber || undefined,
ethSignature: gateState.rainbowToken,
});
}
@@ -400,6 +411,7 @@ const BookingPage = ({
"dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500";
let isSmsReminderNumberNeeded = false;
let isSmsReminderNumberRequired = false;
if (eventType.workflows.length > 0) {
eventType.workflows.forEach((workflowReference) => {
@@ -407,6 +419,7 @@ const BookingPage = ({
workflowReference.workflow.steps.forEach((step) => {
if (step.action === WorkflowActions.SMS_ATTENDEE) {
isSmsReminderNumberNeeded = true;
isSmsReminderNumberRequired = step.numberRequired || false;
return;
}
});
@@ -485,15 +498,25 @@ const BookingPage = ({
<Icon.FiCalendar className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
<div className="text-sm font-medium">
{(rescheduleUid || !eventType.recurringEvent?.freq) &&
parseDate(dayjs(date).tz(timeZone()), i18n)}
`${formatTime(dayjs(date).toDate(), user?.timeFormat, user?.timeZone)}, ${dayjs(
date
).format("dddd, D MMMM YYYY")}`}
{!rescheduleUid &&
eventType.recurringEvent?.freq &&
recurringStrings.slice(0, 5).map((aDate, key) => <p key={key}>{aDate}</p>)}
recurringDates.slice(0, 5).map((aDate, key) => {
return (
<p key={key}>{`${formatTime(aDate, user?.timeFormat, user?.timeZone)}, ${dayjs(
aDate
).format("dddd, D MMMM YYYY")}`}</p>
);
})}
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
<div className="flex">
<Tooltip
content={recurringStrings.slice(5).map((aDate, key) => (
<p key={key}>{aDate}</p>
content={recurringDates.slice(5).map((aDate, key) => (
<p key={key}>{`${formatTime(aDate, user?.timeFormat, user?.timeZone)}, ${dayjs(
aDate
).format("dddd, D MMMM YYYY")}`}</p>
))}>
<p className="dark:text-darkgray-600 text-sm">
{t("plus_more", { count: recurringStrings.length - 5 })}
@@ -516,7 +539,15 @@ const BookingPage = ({
)}
{!!eventType.seatsPerTimeSlot && (
<div className="text-bookinghighlight flex items-start text-sm">
<Icon.FiUser className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
<Icon.FiUser
className={`mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4 ${
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600"
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-bookinghighlight"
}`}
/>
<p
className={`${
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
@@ -577,48 +608,51 @@ const BookingPage = ({
)}
</div>
</div>
<div className="mb-4">
<>
<span className="block text-sm font-medium text-gray-700 dark:text-white">
{t("location")}
</span>
{rescheduleUid ? (
<>
{rescheduleUid ? (
<div className="mb-4">
<span className="block text-sm font-medium text-gray-700 dark:text-white">
{t("location")}
</span>
<p className="mt-1 text-sm text-gray-500">
{getHumanReadableLocationValue(booking?.location, t)}
</p>
) : (
locations.length > 1 && (
<>
{locations.map((location, i) => {
const locationString = locationKeyToString(location);
if (!selectedLocationType) {
bookingForm.setValue("locationType", locations[0].type);
}
if (typeof locationString !== "string") {
// It's possible that location app got uninstalled
return null;
}
return (
<label key={i} className="block">
<input
type="radio"
disabled={!!disableLocations}
className="location dark:bg-darkgray-300 dark:border-darkgray-300 h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
{...bookingForm.register("locationType", { required: true })}
value={location.type}
defaultChecked={i === 0}
/>
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-white">
{locationKeyToString(location)}
</span>
</label>
);
})}
</>
)
)}
</>
</div>
</div>
) : (
locations.length > 1 && (
<div className="mb-4">
<span className="block text-sm font-medium text-gray-700 dark:text-white">
{t("location")}
</span>
{locations.map((location, i) => {
const locationString = locationKeyToString(location);
if (!selectedLocationType) {
bookingForm.setValue("locationType", locations[0].type);
}
if (typeof locationString !== "string") {
// It's possible that location app got uninstalled
return null;
}
return (
<label key={i} className="block">
<input
type="radio"
disabled={!!disableLocations}
className="location dark:bg-darkgray-300 dark:border-darkgray-300 h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
{...bookingForm.register("locationType", { required: true })}
value={location.type}
defaultChecked={i === 0}
/>
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-white">
{locationKeyToString(location)}
</span>
</label>
);
})}
</div>
)
)}
</>
{/* TODO: Change name and id ="phone" to something generic */}
{AttendeeInput && (
<div className="mb-4">
@@ -778,7 +812,7 @@ const BookingPage = ({
<label
htmlFor="smsReminderNumber"
className="block text-sm font-medium text-gray-700 dark:text-white">
{t("number_for_sms_reminders")}
{t("number_sms_notifications")}
</label>
<div className="mt-1">
<PhoneInput<BookingFormValues>
@@ -786,7 +820,7 @@ const BookingPage = ({
name="smsReminderNumber"
placeholder={t("enter_phone_number")}
id="smsReminderNumber"
required
required={isSmsReminderNumberRequired}
/>
</div>
{bookingForm.formState.errors.smsReminderNumber && (
+15 -33
View File
@@ -1,16 +1,12 @@
import { useMutation } from "@tanstack/react-query";
import { RescheduleResponse } from "pages/api/book/request-reschedule";
import React, { useState, Dispatch, SetStateAction } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
import { TextArea } from "@calcom/ui/form/fields";
import Button from "@calcom/ui/v2/core/Button";
import * as fetchWrapper from "@lib/core/http/fetch-wrapper";
import showToast from "@calcom/ui/v2/core/notifications";
interface IRescheduleDialog {
isOpenDialog: boolean;
@@ -23,35 +19,18 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
const utils = trpc.useContext();
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
const [rescheduleReason, setRescheduleReason] = useState("");
const [isLoading, setIsLoading] = useState(false);
const rescheduleApi = useMutation(
async () => {
setIsLoading(true);
try {
const result = await fetchWrapper.post<
{ bookingId: string; rescheduleReason: string },
RescheduleResponse
>("/api/book/request-reschedule", {
bookingId,
rescheduleReason,
});
if (result) {
showToast(t("reschedule_request_sent"), "success");
setIsOpenDialog(false);
}
} catch (error) {
showToast(t("unexpected_error_try_again"), "error");
// @TODO: notify sentry
}
setIsLoading(false);
const { mutate: rescheduleApi, isLoading } = trpc.useMutation("viewer.bookings.requestReschedule", {
async onSuccess() {
showToast(t("reschedule_request_sent"), "success");
setIsOpenDialog(false);
await utils.invalidateQueries(["viewer.bookings"]);
},
{
async onSettled() {
await utils.invalidateQueries(["viewer.bookings"]);
},
}
);
onError() {
showToast(t("unexpected_error_try_again"), "error");
// @TODO: notify sentry
},
});
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
@@ -84,7 +63,10 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
data-testid="send_request"
disabled={isLoading}
onClick={() => {
rescheduleApi.mutate();
rescheduleApi({
bookingId,
rescheduleReason,
});
}}>
{t("send_reschedule_request")}
</Button>
@@ -9,7 +9,6 @@ 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 showToast from "@calcom/lib/notification";
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
@@ -24,6 +23,7 @@ import Dropdown, {
} from "@calcom/ui/Dropdown";
import { Icon } from "@calcom/ui/Icon";
import { Form, InputLeading, TextAreaField, TextField } from "@calcom/ui/form/fields";
import showToast from "@calcom/ui/v2/core/notifications";
import { HttpError } from "@lib/core/http/error";
import { slugify } from "@lib/slugify";
@@ -1,8 +1,8 @@
import { useMutation } from "@tanstack/react-query";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { Switch } from "@calcom/ui/v2";
import showToast from "@calcom/ui/v2/core/notifications";
import classNames from "@lib/classNames";
@@ -22,12 +22,12 @@ const Steps = (props: ISteps) => {
key={`step-${index}`}
onClick={() => navigateToStep(index)}
className={classNames(
"h-1 w-1/4 rounded-[1px] bg-black dark:bg-white",
"h-1 w-full rounded-[1px] bg-black dark:bg-white",
index < currentStep ? "cursor-pointer" : ""
)}
/>
) : (
<div key={`step-${index}`} className="h-1 w-1/4 rounded-[1px] bg-black bg-opacity-25" />
<div key={`step-${index}`} className="h-1 w-full rounded-[1px] bg-black bg-opacity-25" />
);
})}
</div>
@@ -3,7 +3,6 @@ import { Fragment } from "react";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
@@ -11,6 +10,7 @@ import { List } from "@calcom/ui/List";
import { ShellSubHeading } from "@calcom/ui/Shell";
import Switch from "@calcom/ui/Switch";
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
import showToast from "@calcom/ui/v2/core/notifications";
import { QueryCell } from "@lib/QueryCell";
@@ -1,11 +1,11 @@
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { ButtonBaseProps } from "@calcom/ui/Button";
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
import { Dialog } from "@calcom/ui/Dialog";
import showToast from "@calcom/ui/v2/core/notifications";
export default function DisconnectIntegration(props: {
/** Integration credential id */
@@ -1,9 +1,9 @@
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import showToast from "@calcom/lib/notification";
import { ButtonBaseProps } from "@calcom/ui/Button";
import { Dialog } from "@calcom/ui/Dialog";
import showToast from "@calcom/ui/v2/core/notifications";
import DeleteStripeDialogContent from "@components/dialog/DeleteStripeDialogContent";
@@ -2,8 +2,8 @@ import React, { SyntheticEvent, useState } from "react";
import { ErrorCode } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import Button from "@calcom/ui/Button";
import showToast from "@calcom/ui/v2/core/notifications";
const ChangePasswordSection = () => {
const [oldPassword, setOldPassword] = useState("");
@@ -1,8 +1,8 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import Badge from "@calcom/ui/Badge";
import Button from "@calcom/ui/Button";
import showToast from "@calcom/ui/v2/core/notifications";
const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonation: boolean }) => {
const utils = trpc.useContext();
+29 -27
View File
@@ -1,6 +1,14 @@
import merge from "lodash/merge";
import { NextSeo, NextSeoProps } from "next-seo";
import React from "react";
import {
AppImageProps,
constructAppImage,
constructGenericImage,
constructMeetingImage,
MeetingImageProps,
} from "@calcom/lib/OgImages";
import { truncate, truncateOnWord } from "@calcom/lib/text";
import { getSeoImage, seoConfig } from "@lib/config/next-seo.config";
import { getBrowserInfo } from "@lib/core/browser/browser.utils";
@@ -9,11 +17,11 @@ export type HeadSeoProps = {
title: string;
description: string;
siteName?: string;
name?: string;
url?: string;
username?: string;
canonical?: string;
nextSeoProps?: NextSeoProps;
app?: AppImageProps;
meeting?: MeetingImageProps;
};
/**
@@ -63,32 +71,14 @@ const buildSeoMeta = (pageProps: {
};
};
const constructImage = (name: string, description: string, username: string): string => {
return (
encodeURIComponent("Meet **" + name + "** <br>" + description).replace(/'/g, "%27") +
".png?md=1&images=https%3A%2F%2Fcal.com%2Flogo-white.svg&images=" +
(process.env.NEXT_PUBLIC_WEBSITE_URL || process.env.NEXT_PUBLIC_WEBAPP_URL) +
"/" +
username +
"/avatar.png"
);
};
export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
const defaultUrl = getBrowserInfo()?.url;
const image = getSeoImage("default");
const {
title,
description,
name = null,
username = null,
siteName,
canonical = defaultUrl,
nextSeoProps = {},
} = props;
const { title, description, siteName, canonical = defaultUrl, nextSeoProps = {}, app, meeting } = props;
const image = getSeoImage("ogImage") + constructGenericImage({ title, description });
const truncatedDescription = truncateOnWord(description, 158);
const truncatedDescription = description.length > 24 ? description.substring(0, 23) + "..." : description;
const pageTitle = title + " | Cal.com";
let seoObject = buildSeoMeta({
title: pageTitle,
@@ -98,8 +88,20 @@ export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
siteName,
});
if (name && username) {
const pageImage = getSeoImage("ogImage") + constructImage(name, truncatedDescription, username);
if (meeting) {
const pageImage = getSeoImage("ogImage") + constructMeetingImage(meeting);
seoObject = buildSeoMeta({
title: pageTitle,
description: truncatedDescription,
image: pageImage,
canonical,
siteName,
});
}
if (app) {
const pageImage =
getSeoImage("ogImage") + constructAppImage({ ...app, description: truncatedDescription });
seoObject = buildSeoMeta({
title: pageTitle,
description: truncatedDescription,
@@ -1,8 +1,8 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import Badge from "@calcom/ui/Badge";
import Button from "@calcom/ui/Button";
import showToast from "@calcom/ui/v2/core/notifications";
/** @deprecated Use `packages/features/ee/teams/components/DisableTeamImpersonation.tsx` */
const DisableTeamImpersonation = ({ teamId, memberId }: { teamId: number; memberId: number }) => {
+1 -1
View File
@@ -6,7 +6,6 @@ import { useState } from "react";
import TeamAvailabilityModal from "@calcom/features/ee/teams/components/TeamAvailabilityModal";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
@@ -19,6 +18,7 @@ import Dropdown, {
} from "@calcom/ui/Dropdown";
import { Icon } from "@calcom/ui/Icon";
import { Tooltip } from "@calcom/ui/Tooltip";
import showToast from "@calcom/ui/v2/core/notifications";
import useCurrentUserId from "@lib/hooks/useCurrentUserId";
+1 -1
View File
@@ -153,7 +153,7 @@ export default function TeamListItem(props: Props) {
/>
</Tooltip>
<Dropdown>
<DropdownMenuTrigger asChild>
<DropdownMenuTrigger asChild className="radix-state-open:rounded-r-md">
<Button type="button" color="secondary" size="icon" StartIcon={Icon.FiMoreHorizontal} />
</DropdownMenuTrigger>
<DropdownMenuContent hidden={hideDropdown}>
+1 -1
View File
@@ -1,7 +1,6 @@
import React, { useRef, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { objectKeys } from "@calcom/lib/objectKeys";
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { trpc } from "@calcom/trpc/react";
@@ -9,6 +8,7 @@ import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { Icon } from "@calcom/ui/Icon";
import { TextField } from "@calcom/ui/form/fields";
import showToast from "@calcom/ui/v2/core/notifications";
import ImageUploader from "@components/ImageUploader";
import SettingInputContainer from "@components/ui/SettingInputContainer";
@@ -3,12 +3,12 @@ import Link from "next/link";
import { useRouter } from "next/router";
import React from "react";
import showToast from "@calcom/lib/notification";
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { trpc } from "@calcom/trpc/react";
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
import showToast from "@calcom/ui/v2/core/notifications";
import { useLocale } from "@lib/hooks/useLocale";
@@ -1,6 +1,5 @@
import { useState } from "react";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
@@ -12,6 +11,7 @@ import {
DialogFooter,
DialogHeader,
} from "@calcom/ui/Dialog";
import showToast from "@calcom/ui/v2/core/notifications";
import { useLocale } from "@lib/hooks/useLocale";
+5 -2
View File
@@ -4,16 +4,19 @@ interface ILinkTextProps {
href: string;
children: React.ReactNode;
classNameChildren?: string;
target?: string;
}
/**
* This component had to be made in order to make i18n work with next/link
* @see https://github.com/i18next/react-i18next/issues/1090#issuecomment-615426145
**/
export const LinkText = (props: ILinkTextProps) => {
const { href, children, classNameChildren, ...moreProps } = props;
const { target, href, children, classNameChildren, ...moreProps } = props;
return (
<Link href={href || ""} {...moreProps}>
<a className={classNameChildren}>{children}</a>
<a target={target || ""} className={classNameChildren}>
{children}
</a>
</Link>
);
};
@@ -1,5 +1,6 @@
import { FormValues } from "pages/event-types/[type]";
import { Controller, useFormContext } from "react-hook-form";
import { SingleValueProps, OptionProps, components } from "react-select";
import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
@@ -7,6 +8,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui";
import { Badge } from "@calcom/ui/v2";
import Button from "@calcom/ui/v2/core/Button";
import Select from "@calcom/ui/v2/core/form/select";
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
@@ -16,6 +18,35 @@ import { SelectSkeletonLoader } from "@components/v2/availability/SkeletonLoader
type AvailabilityOption = {
label: string;
value: number;
isDefault: boolean;
};
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
const { label, isDefault } = props.data;
return (
<components.Option {...props}>
<span>{label}</span>
{isDefault && (
<Badge variant="blue" className="ml-2">
Default
</Badge>
)}
</components.Option>
);
};
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
const { label, isDefault } = props.data;
return (
<components.SingleValue {...props}>
<span>{label}</span>
{isDefault && (
<Badge variant="blue" className="ml-2">
Default
</Badge>
)}
</components.SingleValue>
);
};
const AvailabilitySelect = ({
@@ -38,6 +69,7 @@ const AvailabilitySelect = ({
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
}));
const value = options.find((option) =>
@@ -53,6 +85,8 @@ const AvailabilitySelect = ({
onChange={props.onChange}
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
value={value}
components={{ Option, SingleValue }}
isMulti={false}
/>
);
};
@@ -95,45 +129,50 @@ export const AvailabilityTab = () => {
/>
</div>
<div className="space-y-4 rounded border p-8 py-6 pt-2">
<div className="space-y-4 rounded border p-4 py-6 pt-2 md:p-8">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
return (
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span className={classNames("w-32 font-medium", !isAvailable && "text-gray-500 opacity-50")}>
<span
className={classNames(
"w-20 font-medium sm:w-32",
!isAvailable && "text-gray-500 opacity-50"
)}>
{day}
</span>
{isLoading ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3">
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="flex items-center leading-4">
<span className="w-28">{format(dayRange.startTime)}</span>
<span className="">-</span>
<span className="w-16 sm:w-28 sm:text-left">{format(dayRange.startTime)}</span>
<span className="ml-4">-</span>
<div className="ml-6">{format(dayRange.endTime)}</div>
</div>
))}
</div>
) : (
<span className="text-gray-500 opacity-50 ">{t("unavailable")}</span>
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr />
<div className="flex justify-between">
<span className="flex items-center text-sm text-gray-600">
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
<Icon.FiGlobe className="mr-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
<Button
href={`/availability/${scheduleId}`}
href={`/availability/${schedule?.schedule.id}`}
color="minimal"
EndIcon={Icon.FiExternalLink}
target="_blank"
className="justify-center border sm:border-0"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
@@ -1,6 +1,7 @@
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 { z } from "zod";
@@ -24,6 +25,7 @@ import { Select } from "@calcom/ui/v2/";
import { QueryCell } from "@lib/QueryCell";
import { LinkText } from "@components/ui/LinkText";
import CheckboxField from "@components/ui/form/CheckboxField";
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
@@ -308,6 +310,17 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
}}
/>
{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={() => {
@@ -1,12 +1,13 @@
import autoAnimate from "@formkit/auto-animate";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { EventTypeCustomInput } from "@prisma/client/";
import Link from "next/link";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
@@ -14,16 +15,17 @@ import { Icon } from "@calcom/ui";
import {
Button,
CustomInputItem,
DestinationCalendarSelector,
Dialog,
DialogContent,
Label,
SettingsToggle,
showToast,
Skeleton,
Switch,
TextField,
Tooltip,
} from "@calcom/ui/v2";
import CheckboxField from "@calcom/ui/v2/core/form/Checkbox";
import CustomInputTypeForm from "@components/v2/eventtype/CustomInputTypeForm";
@@ -42,7 +44,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const [seatsInputVisible, setSeatsInputVisible] = useState(!!eventType.seatsPerTimeSlot);
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
eventType.customInputs.sort((a, b) => a.id - b.id) || []
);
@@ -50,13 +51,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const animationRef = useRef(null);
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
useEffect(() => {
animationRef.current && autoAnimate(animationRef.current);
}, [animationRef]);
const removeCustom = (index: number) => {
formMethods.getValues("customInputs").splice(index, 1);
customInputs.splice(index, 1);
@@ -122,80 +118,61 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
</div>
<hr />
<div className="">
<div className="flex space-x-3 ">
<Switch
checked={customInputs.length > 0}
fitToHeight={true}
onCheckedChange={(e) => {
if (e && customInputs.length === 0) {
// Push a placeholders
setSelectedCustomInput(undefined);
setSelectedCustomInputModalOpen(true);
} else if (!e) {
setCustomInputs([]);
formMethods.setValue("customInputs", []);
}
}}
/>
<div className="flex flex-col">
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
{t("additional_inputs")}
</Skeleton>
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
{t("additional_input_description")}
</Skeleton>
</div>
</div>
<ul className="my-4" ref={animationRef}>
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
<CustomInputItem
key={idx}
question={customInput.label}
type={customInput.type}
required={customInput.required}
editOnClick={() => {
setSelectedCustomInput(customInput);
setSelectedCustomInputModalOpen(true);
}}
deleteOnClick={() => removeCustom(idx)}
/>
))}
</ul>
{customInputs.length > 0 && (
<Button
StartIcon={Icon.FiPlus}
color="minimal"
type="button"
onClick={() => {
<SettingsToggle
title={t("additional_inputs")}
description={t("additional_input_description")}
checked={customInputs.length > 0}
onCheckedChange={(e) => {
if (e && customInputs.length === 0) {
// Push a placeholders
setSelectedCustomInput(undefined);
setSelectedCustomInputModalOpen(true);
}}>
Add an input
</Button>
)}
} else if (!e) {
setCustomInputs([]);
formMethods.setValue("customInputs", []);
}
}}>
<ul className="my-4">
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
<CustomInputItem
key={idx}
question={customInput.label}
type={customInput.type}
required={customInput.required}
editOnClick={() => {
setSelectedCustomInput(customInput);
setSelectedCustomInputModalOpen(true);
}}
deleteOnClick={() => removeCustom(idx)}
/>
))}
</ul>
{customInputs.length > 0 && (
<Button
StartIcon={Icon.FiPlus}
color="minimal"
type="button"
onClick={() => {
setSelectedCustomInput(undefined);
setSelectedCustomInputModalOpen(true);
}}>
Add an input
</Button>
)}
</SettingsToggle>
</div>
<hr />
<Controller
name="requiresConfirmation"
defaultValue={eventType.requiresConfirmation}
render={({ field: { value, onChange } }) => (
<div className="flex space-x-3">
<Switch
name="requireConfirmation"
checked={value}
onCheckedChange={(e) => onChange(e)}
disabled={seatsEnabled}
fitToHeight={true}
/>
<div className="flex flex-col">
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
{t("requires_confirmation")}
</Skeleton>
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
{t("requires_confirmation_description")}
</Skeleton>
</div>
</div>
<SettingsToggle
title={t("requires_confirmation")}
description={t("requires_confirmation_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
disabled={seatsEnabled}
/>
)}
/>
<hr />
@@ -204,23 +181,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
control={formMethods.control}
defaultValue={eventType.disableGuests}
render={({ field: { value, onChange } }) => (
<div className="flex space-x-3 ">
<Switch
name="disableGuests"
fitToHeight={true}
checked={value}
onCheckedChange={(e) => onChange(e)}
disabled={seatsEnabled}
/>
<div className="flex flex-col">
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
{t("disable_guests")}
</Skeleton>
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
{t("disable_guests_description")}
</Skeleton>
</div>
</div>
<SettingsToggle
title={t("disable_guests")}
description={t("disable_guests_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
disabled={seatsEnabled}
/>
)}
/>
@@ -230,22 +197,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
control={formMethods.control}
defaultValue={eventType.hideCalendarNotes}
render={({ field: { value, onChange } }) => (
<div className="flex space-x-3 ">
<Switch
name="hideCalendarNotes"
fitToHeight={true}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
<div className="flex flex-col">
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
{t("disable_notes")}
</Skeleton>
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
{t("disable_notes_description")}
</Skeleton>
</div>
</div>
<SettingsToggle
title={t("disable_notes")}
description={t("disable_notes_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
)}
/>
<hr />
@@ -255,20 +212,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
defaultValue={!!eventType.metadata.additionalNotesRequired}
render={({ field: { value, onChange } }) => (
<div className="flex space-x-3 ">
<Switch
name="additionalNotesRequired"
fitToHeight={true}
checked={value}
<SettingsToggle
title={t("require_additional_notes")}
description={t("require_additional_notes_description")}
checked={!!value}
onCheckedChange={(e) => onChange(e)}
/>
<div className="flex flex-col">
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
{t("require_additional_notes")}
</Skeleton>
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
{t("require_additional_notes_description")}
</Skeleton>
</div>
</div>
)}
/>
@@ -276,32 +225,21 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<Controller
name="successRedirectUrl"
control={formMethods.control}
defaultValue={hashedUrl}
render={({ field: { value, onChange } }) => (
<>
<div className="flex space-x-3 ">
<Switch
name="successRedirectUrlCheck"
fitToHeight={true}
defaultChecked={redirectUrlVisible}
onCheckedChange={(e) => {
setRedirectUrlVisible(e);
onChange(e ? value : "");
}}
/>
<div className="flex flex-col">
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
{t("redirect_success_booking")}
</Skeleton>
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
{t("redirect_url_description")}
</Skeleton>
</div>
</div>
{redirectUrlVisible && (
<div className="">
<SettingsToggle
title={t("redirect_success_booking")}
description={t("redirect_url_description")}
checked={redirectUrlVisible}
onCheckedChange={(e) => {
setRedirectUrlVisible(e);
onChange(e ? value : "");
}}>
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
<div className="lg:-ml-2">
<TextField
label={t("redirect_success_booking")}
labelSrOnly
placeholder={t("external_redirect_url")}
required={redirectUrlVisible}
type="text"
@@ -309,130 +247,107 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
{...formMethods.register("successRedirectUrl")}
/>
</div>
)}
</SettingsToggle>
</>
)}
/>
<hr />
<Controller
name="hashedLink"
control={formMethods.control}
defaultValue={hashedUrl}
render={({ field: { value, onChange } }) => (
<>
<div className="flex space-x-3 ">
<Switch
data-testid="hashedLinkCheck"
name="hashedLinkCheck"
fitToHeight={true}
defaultChecked={!!value}
onCheckedChange={(e) => {
setHashedLinkVisible(e);
onChange(e ? hashedUrl : undefined);
}}
/>
<div className="flex flex-col">
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
{t("private_link")}
</Skeleton>
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
{t("private_link_description")}
</Skeleton>
</div>
</div>
{hashedLinkVisible && (
<div className="">
<TextField
disabled
name="hashedLink"
label={t("private_link_label")}
data-testid="generated-hash-url"
type="text"
hint={t("private_link_hint")}
defaultValue={placeholderHashedLink}
addOnSuffix={
<Tooltip
content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Button
color="minimal"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
showToast(t("private_link_copied"), "success");
} else {
showToast(t("enabled_after_update_description"), "warning");
}
}}
className="hover:stroke-3 hover:bg-transparent hover:text-black"
type="button">
<Icon.FiCopy />
</Button>
</Tooltip>
}
/>
</div>
)}
</>
)}
/>
<SettingsToggle
data-testid="hashedLinkCheck"
title={t("private_link")}
description={t("private_link_description")}
checked={hashedLinkVisible}
onCheckedChange={(e) => {
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
setHashedLinkVisible(e);
}}>
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
<div className="lg:-ml-2">
<TextField
disabled
name="hashedLink"
label={t("private_link_label")}
data-testid="generated-hash-url"
labelSrOnly
type="text"
hint={t("private_link_hint")}
defaultValue={placeholderHashedLink}
addOnSuffix={
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Button
color="minimal"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
showToast(t("private_link_copied"), "success");
} else {
showToast(t("enabled_after_update_description"), "warning");
}
}}
className="hover:stroke-3 hover:bg-transparent hover:text-black"
type="button">
<Icon.FiCopy />
</Button>
</Tooltip>
}
/>
</div>
</SettingsToggle>
<hr />
<Controller
name="seatsPerTimeSlotEnabled"
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div className="flex space-x-3">
<Switch
name="seatsPerTimeSlotEnabled"
checked={value}
onCheckedChange={(e) => {
setSeatsInputVisible(e);
// Enabling seats will disable guests and requiring confimation until fully supported
if (e) {
formMethods.setValue("disableGuests", true);
formMethods.setValue("requiresConfirmation", false);
formMethods.setValue("seatsPerTimeSlot", 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
}
onChange(e);
}}
fitToHeight={true}
<SettingsToggle
title={t("offer_seats")}
description={t("offer_seats_description")}
checked={value}
onCheckedChange={(e) => {
// Enabling seats will disable guests and requiring confimation until fully supported
if (e) {
formMethods.setValue("disableGuests", true);
formMethods.setValue("requiresConfirmation", false);
formMethods.setValue("seatsPerTimeSlot", 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
formMethods.setValue("disableGuests", false);
}
onChange(e);
}}>
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div className="lg:-ml-2">
<TextField
required
name="seatsPerTimeSlot"
labelSrOnly
label={t("number_of_seats")}
type="number"
defaultValue={value || 2}
addOnSuffix={<>{t("seats")}</>}
onChange={(e) => {
onChange(Number(e.target.value));
}}
/>
<div className="mt-2">
<CheckboxField
description={t("show_attendees")}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
defaultChecked={!!eventType.seatsShowAttendees}
/>
</div>
</div>
)}
/>
<div className="flex flex-col">
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
{t("offer_seats")}
</Skeleton>
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
{t("offer_seats_description")}
</Skeleton>
</div>
</div>
</SettingsToggle>
)}
/>
{seatsInputVisible && (
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div className="">
<TextField
required
name="seatsPerTimeSlot"
label={t("number_of_seats")}
type="number"
defaultValue={value || 2}
addOnSuffix={<>{t("seats")}</>}
onChange={(e) => {
onChange(Number(e.target.value));
}}
/>
</div>
)}
/>
)}
{showEventNameTip && (
<Dialog open={showEventNameTip} onOpenChange={setShowEventNameTip}>
<DialogContent
@@ -1,7 +1,7 @@
import autoAnimate from "@formkit/auto-animate";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef, useState } from "react";
import { useState } from "react";
import { useFormContext, Controller, useWatch } from "react-hook-form";
import { classNames } from "@calcom/lib";
@@ -9,7 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { PeriodType } from "@calcom/prisma/client";
import type { BookingLimit } from "@calcom/types/Calendar";
import { Icon } from "@calcom/ui";
import { Select, Switch, Label, Input, MinutesField, Button } from "@calcom/ui/v2";
import { Select, Switch, Label, Input, MinutesField, Button, SettingsToggle } from "@calcom/ui/v2";
import DateRangePicker from "@calcom/ui/v2/core/form/date-range-picker/DateRangePicker";
export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">) => {
@@ -159,149 +159,106 @@ export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">)
<hr className="my-8" />
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<fieldset className="block flex-col sm:flex">
<div className="flex space-x-3">
<Controller
name="bookingLimits"
control={formMethods.control}
render={({ field: { value } }) => (
<Switch
fitToHeight={true}
checked={Object.keys(value ?? {}).length > 0}
onCheckedChange={(active) => {
if (active) {
formMethods.setValue("bookingLimits", {
PER_DAY: 1,
});
} else {
formMethods.setValue("bookingLimits", undefined);
}
}}
/>
)}
/>
<div className="">
<Label className="text-sm font-semibold leading-none text-black">
{t("limit_booking_frequency")}
</Label>
<p className="-mt-2 text-sm leading-normal text-gray-600">
{t("limit_booking_frequency_description")}
</p>
</div>
</div>
<div className="mt-4 lg:ml-14">
<Controller
name="bookingLimits"
control={formMethods.control}
render={({ field: { value } }) => (
<SettingsToggle
title={t("limit_booking_frequency")}
description={t("limit_booking_frequency_description")}
checked={Object.keys(value ?? {}).length > 0}
onCheckedChange={(active) => {
if (active) {
formMethods.setValue("bookingLimits", {
PER_DAY: 1,
});
} else {
formMethods.setValue("bookingLimits", {});
}
}}>
<BookingLimits />
</div>
</fieldset>
</div>
</SettingsToggle>
)}
/>
<hr className="my-8" />
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<fieldset className="block flex-col sm:flex">
<div className="flex space-x-3">
<Controller
name="periodType"
control={formMethods.control}
defaultValue={periodType?.type !== "UNLIMITED" ? "ROLLING" : "UNLIMITED"}
render={({ field: { value } }) => (
<Switch
fitToHeight={true}
checked={value !== "UNLIMITED"}
onCheckedChange={(bool) =>
formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")
}
/>
)}
/>
<div className="">
<Label className="text-sm font-semibold leading-none text-black">Limit Future Bookings</Label>
<p className="-mt-2 text-sm leading-normal text-gray-600">
Limit how far in the future people can book a time
</p>
</div>
</div>
<div className="mt-4 lg:ml-14">
<Controller
name="periodType"
control={formMethods.control}
defaultValue={periodType?.type}
render={() => (
<RadioGroup.Root
defaultValue={watchPeriodType}
value={watchPeriodType}
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
{PERIOD_TYPES.map((period) => {
if (period.type === "UNLIMITED") return null;
return (
<div
className={classNames(
"mb-2 flex flex-wrap items-center text-sm",
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
)}
key={period.type}>
<RadioGroup.Item
id={period.type}
value={period.type}
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>
{period.prefix ? <span>{period.prefix}&nbsp;</span> : null}
{period.type === "ROLLING" && (
<div className="flex h-9">
<Input
type="number"
className="block w-16 rounded-md border-gray-300 py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
placeholder="30"
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
/>
<select
id=""
className="block h-9 w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
{...formMethods.register("periodCountCalendarDays")}
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
<option value="1">{t("calendar_days")}</option>
<option value="0">{t("business_days")}</option>
</select>
</div>
)}
{period.type === "RANGE" && (
<div className="inline-flex space-x-2 ltr:ml-2 rtl:mr-2 rtl:space-x-reverse">
<Controller
name="periodDates"
control={formMethods.control}
defaultValue={periodDates}
render={() => (
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
endDate={formMethods.getValues("periodDates").endDate}
onDatesChange={({ startDate, endDate }) => {
formMethods.setValue("periodDates", {
startDate,
endDate,
});
}}
/>
)}
/>
</div>
)}
{period.suffix ? (
<span className="ltr:ml-2 rtl:mr-2">&nbsp;{period.suffix}</span>
) : null}
<Controller
name="periodType"
control={formMethods.control}
render={({ field: { value } }) => (
<SettingsToggle
title={t("limit_future_bookings")}
description={t("limit_future_bookings_description")}
checked={value !== "UNLIMITED"}
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
<RadioGroup.Root
defaultValue={watchPeriodType}
value={watchPeriodType}
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
{PERIOD_TYPES.map((period) => {
if (period.type === "UNLIMITED") return null;
return (
<div
className={classNames(
"mb-2 flex flex-wrap items-center text-sm",
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
)}
key={period.type}>
<RadioGroup.Item
id={period.type}
value={period.type}
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>
{period.prefix ? <span>{period.prefix}&nbsp;</span> : null}
{period.type === "ROLLING" && (
<div className="flex h-9">
<Input
type="number"
className="block w-16 rounded-md border-gray-300 py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
placeholder="30"
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
/>
<select
id=""
className="block h-9 w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
{...formMethods.register("periodCountCalendarDays")}
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
<option value="1">{t("calendar_days")}</option>
<option value="0">{t("business_days")}</option>
</select>
</div>
);
})}
</RadioGroup.Root>
)}
/>
</div>
</fieldset>
</div>
)}
{period.type === "RANGE" && (
<div className="inline-flex space-x-2 ltr:ml-2 rtl:mr-2 rtl:space-x-reverse">
<Controller
name="periodDates"
control={formMethods.control}
defaultValue={periodDates}
render={() => (
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
endDate={formMethods.getValues("periodDates").endDate}
onDatesChange={({ startDate, endDate }) => {
formMethods.setValue("periodDates", {
startDate,
endDate,
});
}}
/>
)}
/>
</div>
)}
{period.suffix ? <span className="ltr:ml-2 rtl:mr-2">&nbsp;{period.suffix}</span> : null}
</div>
);
})}
</RadioGroup.Root>
</SettingsToggle>
)}
/>
</div>
);
};
@@ -311,12 +268,9 @@ type BookingLimitsKey = keyof BookingLimit;
const BookingLimits = () => {
const { watch, setValue, control } = useFormContext<FormValues>();
const watchBookingLimits = watch("bookingLimits");
const animateRef = useRef(null);
const { t } = useLocale();
useEffect(() => {
animateRef.current && autoAnimate(animateRef.current);
}, [animateRef]);
const [animateRef] = useAutoAnimate<HTMLUListElement>();
const BOOKING_LIMIT_OPTIONS: {
value: keyof BookingLimit;
@@ -1,8 +1,8 @@
import autoAnimate from "@formkit/auto-animate";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef, useState } from "react";
import { useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form";
import { z } from "zod";
@@ -85,11 +85,8 @@ export const EventSetupTab = (
const Locations = () => {
const { t } = useLocale();
const animationRef = useRef(null);
useEffect(() => {
animationRef.current && autoAnimate(animationRef.current);
}, [animationRef]);
const [animationRef] = useAutoAnimate<HTMLUListElement>();
const validLocations = formMethods.getValues("locations").filter((location) => {
const eventLocation = getEventLocationType(location.type);
@@ -135,14 +132,14 @@ export const EventSetupTab = (
}
return (
<li key={location.type} className="mb-2 rounded-md border border-neutral-300 py-1.5 px-2">
<div className="flex justify-between">
<div className="flex max-w-full justify-between">
<div key={index} className="flex flex-grow items-center">
<img
src={eventLocationType.iconUrl}
className="h-6 w-6"
alt={`${eventLocationType.label} logo`}
/>
<span className="text-sm ltr:ml-2 rtl:mr-2">
<span className="truncate text-sm ltr:ml-2 rtl:mr-2">
{location[eventLocationType.defaultValueVariable] || eventLocationType.label}
</span>
</div>
@@ -23,6 +23,7 @@ import {
} from "@calcom/ui/v2";
import ConfirmationDialogContent from "@calcom/ui/v2/core/ConfirmationDialogContent";
import { Dialog } from "@calcom/ui/v2/core/Dialog";
import Divider from "@calcom/ui/v2/core/Divider";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
@@ -172,11 +173,11 @@ function EventTypeSingleLayout({
subtitle={eventType.description || ""}
CTA={
<div className="flex items-center justify-end">
<div className="flex items-center rounded-md px-2 sm:hover:bg-gray-100">
<div className="hidden items-center rounded-md px-2 sm:flex sm:hover:bg-gray-100">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
className="mt-2 hidden cursor-pointer self-center pr-2 sm:inline">
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
{t("hide_from_profile")}
</Skeleton>
<Switch
@@ -231,10 +232,10 @@ function EventTypeSingleLayout({
/>
</ButtonGroup>
<VerticalDivider />
<VerticalDivider className="hidden lg:block" />
<Dropdown>
<DropdownMenuTrigger className="focus:ring-brand-900 block h-9 w-9 justify-center rounded-md border border-gray-200 bg-transparent text-gray-700 focus:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-1 lg:hidden">
<DropdownMenuTrigger className="block h-9 w-9 justify-center rounded-md border border-gray-200 bg-transparent text-gray-700 lg:hidden">
<Icon.FiMoreHorizontal className="group-hover:text-gray-800" />
</DropdownMenuTrigger>
<DropdownMenuContent>
@@ -264,6 +265,22 @@ function EventTypeSingleLayout({
{t("delete")}
</Button>
</DropdownMenuItem>
<Divider />
<div className="flex items-center rounded-md py-1.5 px-4 sm:hidden sm:hover:bg-gray-100">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
className="mt-2 inline cursor-pointer self-center pr-2 sm:hidden">
{t("hide_from_profile")}
</Skeleton>
<Switch
id="hiddenSwitch"
defaultChecked={formMethods.getValues("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", e);
}}
/>
</div>
</DropdownMenuContent>
</Dropdown>
<div className="border-l-2 border-gray-300" />
@@ -6,7 +6,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Frequency } from "@calcom/prisma/zod-utils";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Alert } from "@calcom/ui/Alert";
import { Label, Select, Switch } from "@calcom/ui/v2";
import { Label, Select, Switch, SettingsToggle } from "@calcom/ui/v2";
type RecurringEventControllerProps = {
recurringEvent: RecurringEvent | null;
@@ -36,95 +36,85 @@ export default function RecurringEventController({
<Alert severity="warning" title={t("warning_payment_recurring_event")} />
) : (
<>
<div className="flex space-x-3 ">
<Switch
name="requireConfirmation"
data-testid="recurring-event-check"
fitToHeight={true}
checked={recurringEventState !== null}
onCheckedChange={(e) => {
if (!e) {
formMethods.setValue("recurringEvent", null);
setRecurringEventState(null);
} else {
const newVal = recurringEvent || {
interval: 1,
count: 12,
freq: Frequency.WEEKLY,
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}
}}
/>
<div className="flex flex-col">
<Label className="text-sm font-semibold leading-none text-black">
{t("recurring_event")}
</Label>
<p className="-mt-2 text-sm leading-normal text-gray-600">
{t("recurring_event_description")}
</p>
{recurringEventState && (
<div data-testid="recurring-event-collapsible" className="mt-4 text-sm">
<div className="flex items-center">
<p className="mr-2 text-neutral-900">{t("repeats_every")}</p>
<input
type="number"
min="1"
max="20"
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
defaultValue={recurringEventState.interval}
onChange={(event) => {
const newVal = {
...recurringEventState,
interval: parseInt(event?.target.value),
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}}
/>
<Select
options={recurringEventFreqOptions}
value={recurringEventFreqOptions[recurringEventState.freq]}
isSearchable={false}
className="w-18 block h-[36px] min-w-0 rounded-md text-sm"
onChange={(event) => {
const newVal = {
...recurringEventState,
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}}
/>
</div>
<div className="mt-4 flex items-center">
<p className="mr-2 text-neutral-900">{t("for_a_maximum_of")}</p>
<input
type="number"
min="1"
max="20"
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
defaultValue={recurringEventState.count}
onChange={(event) => {
const newVal = {
...recurringEventState,
count: parseInt(event?.target.value),
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}}
/>
<p className="mr-2 text-neutral-900">
{t("events", {
count: recurringEventState.count,
})}
</p>
</div>
<SettingsToggle
title={t("recurring_event")}
description={t("recurring_event_description")}
checked={recurringEventState !== null}
data-testid="recurring-event-check"
onCheckedChange={(e) => {
if (!e) {
formMethods.setValue("recurringEvent", null);
setRecurringEventState(null);
} else {
const newVal = recurringEvent || {
interval: 1,
count: 12,
freq: Frequency.WEEKLY,
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}
}}>
{recurringEventState && (
<div data-testid="recurring-event-collapsible" className="text-sm">
<div className="flex items-center">
<p className="mr-2 text-neutral-900">{t("repeats_every")}</p>
<input
type="number"
min="1"
max="20"
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
defaultValue={recurringEventState.interval}
onChange={(event) => {
const newVal = {
...recurringEventState,
interval: parseInt(event?.target.value),
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}}
/>
<Select
options={recurringEventFreqOptions}
value={recurringEventFreqOptions[recurringEventState.freq]}
isSearchable={false}
className="w-18 block h-[36px] min-w-0 rounded-md text-sm"
onChange={(event) => {
const newVal = {
...recurringEventState,
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}}
/>
</div>
)}
</div>
</div>
<div className="mt-4 flex items-center">
<p className="mr-2 text-neutral-900">{t("for_a_maximum_of")}</p>
<input
type="number"
min="1"
max="20"
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
defaultValue={recurringEventState.count}
onChange={(event) => {
const newVal = {
...recurringEventState,
count: parseInt(event?.target.value),
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}}
/>
<p className="mr-2 text-neutral-900">
{t("events", {
count: recurringEventState.count,
})}
</p>
</div>
</div>
)}
</SettingsToggle>
</>
)}
</div>
-40
View File
@@ -1,40 +0,0 @@
import jackson, { IAPIController, IOAuthController, JacksonOption } from "@boxyhq/saml-jackson";
import { BASE_URL } from "@lib/config/constants";
import { samlDatabaseUrl } from "@lib/saml";
// Set the required options. Refer to https://github.com/boxyhq/jackson#configuration for the full list
const opts: JacksonOption = {
externalUrl: BASE_URL,
samlPath: "/api/auth/saml/callback",
db: {
engine: "sql",
type: "postgres",
url: samlDatabaseUrl,
encryptionKey: process.env.CALENDSO_ENCRYPTION_KEY,
},
samlAudience: "https://saml.cal.com",
};
let apiController: IAPIController;
let oauthController: IOAuthController;
const g = global as any;
export default async function init() {
if (!g.apiController || !g.oauthController) {
const ret = await jackson(opts);
apiController = ret.apiController;
oauthController = ret.oauthController;
g.apiController = apiController;
g.oauthController = oauthController;
} else {
apiController = g.apiController;
oauthController = g.oauthController;
}
return {
apiController,
oauthController,
};
}
@@ -1,22 +1,46 @@
import { AppsStatus } from "@calcom/types/Calendar";
import * as fetch from "@lib/core/http/fetch-wrapper";
import { BookingCreateBody, BookingResponse } from "@lib/types/booking";
type ExtendedBookingCreateBody = BookingCreateBody & { noEmail?: boolean; recurringCount?: number };
const createRecurringBooking = async (data: ExtendedBookingCreateBody[]) => {
return Promise.all(
data.map((booking, key) => {
// We only want to send the first occurrence of the meeting at the moment, not all at once
if (key === 0) {
return fetch.post<ExtendedBookingCreateBody, BookingResponse>("/api/book/event", booking);
} else {
return fetch.post<ExtendedBookingCreateBody, BookingResponse>("/api/book/event", {
...booking,
noEmail: true,
});
}
})
);
const createdBookings: BookingResponse[] = [];
// Reversing to accumulate results for noEmail instances first, to then lastly, create the
// emailed booking taking into account accumulated results to send app status accurately
for (let key = 0; key < data.length; key++) {
const booking = data[key];
if (key === data.length - 1) {
const calcAppsStatus: { [key: string]: AppsStatus } = createdBookings
.flatMap((book) => (book.appsStatus !== undefined ? book.appsStatus : []))
.reduce((prev, curr) => {
if (prev[curr.type]) {
prev[curr.type].failures += curr.failures;
prev[curr.type].success += curr.success;
} else {
prev[curr.type] = curr;
}
return prev;
}, {} as { [key: string]: AppsStatus });
const appsStatus = Object.values(calcAppsStatus);
const response = await fetch.post<
ExtendedBookingCreateBody & { appsStatus: AppsStatus[] },
BookingResponse
>("/api/book/event", {
...booking,
appsStatus,
});
createdBookings.push(response);
} else {
const response = await fetch.post<ExtendedBookingCreateBody, BookingResponse>("/api/book/event", {
...booking,
noEmail: true,
});
createdBookings.push(response);
}
}
return createdBookings;
};
export default createRecurringBooking;
+18 -14
View File
@@ -20,6 +20,15 @@ export const parseDate = (date: string | null | Dayjs, i18n: I18n) => {
return processDate(date, i18n);
};
// tzid is currently broken in rrule library.
// @see https://github.com/jakubroztocil/rrule/issues/523
const dateWithZone = (d: Date, timeZone?: string) => {
const dateInLocalTZ = new Date(d.toLocaleString("en-US", { timeZone: "UTC" }));
const dateInTargetTZ = new Date(d.toLocaleString("en-US", { timeZone: timeZone || "UTC" }));
const tzOffset = dateInTargetTZ.getTime() - dateInLocalTZ.getTime();
return new Date(d.getTime() - tzOffset);
};
export const parseRecurringDates = (
{
startDate,
@@ -39,22 +48,16 @@ export const parseRecurringDates = (
const rule = new RRule({
...restRecurringEvent,
count: recurringCount,
dtstart: new Date(
Date.UTC(
dayjs.utc(startDate).get("year"),
dayjs.utc(startDate).get("month"),
dayjs.utc(startDate).get("date"),
dayjs.utc(startDate).get("hour"),
dayjs.utc(startDate).get("minute")
)
),
dtstart: dayjs(startDate).utc(true).toDate(),
});
// UTC times with tzOffset applied to account for DST
const times = rule.all().map((t) => dateWithZone(t, timeZone));
const dateStrings = times.map((t) => {
// undo DST diffs for localized display.
return processDate(dayjs.utc(t).tz(timeZone), i18n);
});
const utcOffset = dayjs(startDate).tz(timeZone).utcOffset();
const dateStrings = rule.all().map((r) => {
return processDate(dayjs.utc(r).utcOffset(utcOffset), i18n);
});
return [dateStrings, rule.all()];
return [dateStrings, times];
};
export const extractRecurringDates = (
@@ -66,6 +69,7 @@ export const extractRecurringDates = (
timeZone: string | undefined,
i18n: I18n
): [string[], Date[]] => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { count = 0, ...rest } =
booking.eventType.recurringEvent !== null ? booking.eventType.recurringEvent : {};
const recurringInfo = booking.recurringBookings.find(
-59
View File
@@ -1,59 +0,0 @@
import { PrismaClient } from "@prisma/client";
import { TRPCError } from "@calcom/trpc/server";
import { BASE_URL } from "@lib/config/constants";
export const samlDatabaseUrl = process.env.SAML_DATABASE_URL || "";
export const samlLoginUrl = BASE_URL;
export const isSAMLLoginEnabled = samlDatabaseUrl.length > 0;
export const samlTenantID = "Cal.com";
export const samlProductID = "Cal.com";
const samlAdmins = (process.env.SAML_ADMINS || "").split(",");
export const hostedCal = BASE_URL === "https://app.cal.com";
export const tenantPrefix = "team-";
export const isSAMLAdmin = (email: string) => {
for (const admin of samlAdmins) {
if (admin.toLowerCase() === email.toLowerCase() && admin.toUpperCase() === email.toUpperCase()) {
return true;
}
}
return false;
};
export const samlTenantProduct = async (prisma: PrismaClient, email: string) => {
const user = await prisma.user.findUnique({
where: {
email,
},
select: {
id: true,
invitedTo: true,
},
});
if (!user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Unauthorized Request",
});
}
if (!user.invitedTo) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Could not find a SAML Identity Provider for your email. Please contact your admin to ensure you have been given access to Cal",
});
}
return {
tenant: tenantPrefix + user.invitedTo,
product: samlProductID,
};
};
+3
View File
@@ -1,5 +1,7 @@
import { Attendee, Booking } from "@prisma/client";
import { AppsStatus } from "@calcom/types/Calendar";
export type BookingCreateBody = {
email: string;
end: string;
@@ -33,4 +35,5 @@ export type BookingCreateBody = {
export type BookingResponse = Booking & {
paymentUid?: string;
attendees: Attendee[];
appsStatus?: AppsStatus[];
};
+21
View File
@@ -0,0 +1,21 @@
import { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from "next";
export type EmbedProps = {
isEmbed?: boolean;
};
export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
const ssrResponse = await getServerSideProps(context);
if (!("props" in ssrResponse)) {
return ssrResponse;
}
return {
...ssrResponse,
props: {
...ssrResponse.props,
isEmbed: true,
},
};
};
}
+7
View File
@@ -23,6 +23,13 @@ const middleware: NextMiddleware = async (req) => {
}
}
// Ensure that embed query param is there in when /embed is added.
// query param is the way in which client side code knows that it is in embed mode.
if (url.pathname.endsWith("/embed") && typeof url.searchParams.get("embed") !== "string") {
url.searchParams.set("embed", "");
return NextResponse.redirect(url);
}
// Don't 404 old routing_forms links
if (url.pathname.startsWith("/apps/routing_forms")) {
url.pathname = url.pathname.replace("/apps/routing_forms", "/apps/routing-forms");
+2 -2
View File
@@ -134,8 +134,8 @@ const nextConfig = {
destination: "/api/user/avatar?teamname=:teamname",
},
{
source: "/forms/:formId",
destination: "/apps/routing-forms/routing-link/:formId",
source: "/forms/:formQuery*",
destination: "/apps/routing-forms/routing-link/:formQuery*",
},
{
source: "/router",
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.1.0",
"version": "2.1.2",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -23,7 +23,7 @@
"yarn": ">=1.19.0 < 2.0.0"
},
"dependencies": {
"@boxyhq/saml-jackson": "0.3.6",
"@boxyhq/saml-jackson": "1.3.1",
"@calcom/app-store": "*",
"@calcom/app-store-cli": "*",
"@calcom/core": "*",
@@ -60,6 +60,7 @@
"@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9",
"@vercel/edge-functions-ui": "^0.2.1",
"@vercel/og": "^0.0.19",
"@wojtekmaj/react-daterange-picker": "^3.3.1",
"accept-language-parser": "^1.5.0",
"async": "^3.2.4",
@@ -119,7 +120,7 @@
"tailwindcss-radix": "^2.6.0",
"uuid": "^8.3.2",
"web3": "^1.7.5",
"zod": "^3.18.0"
"zod": "^3.19.1"
},
"devDependencies": {
"@babel/core": "^7.18.10",
+20 -17
View File
@@ -13,6 +13,7 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import CustomBranding from "@calcom/lib/CustomBranding";
import defaultEvents, {
getDynamicEventDescription,
@@ -30,6 +31,7 @@ import { BadgeCheckIcon, Icon } from "@calcom/ui/Icon";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr";
import AvatarGroup from "@components/ui/AvatarGroup";
import { AvatarSSR } from "@components/ui/AvatarSSR";
@@ -38,7 +40,7 @@ import { ssrInit } from "@server/lib/ssr";
const EventTypeDescription = dynamic(() => import("@calcom/ui/v2/modules/event-types/EventTypeDescription"));
const HeadSeo = dynamic(() => import("@components/seo/head-seo"));
export default function User(props: inferSSRProps<typeof getServerSideProps>) {
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
useTheme(user.theme);
@@ -86,7 +88,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
</ul>
);
const isEmbed = useIsEmbed();
const isEmbed = useIsEmbed(props.isEmbed);
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
@@ -102,7 +104,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
}
}, [telemetry, router.asPath]);
const isEventListEmpty = eventTypes.length === 0;
return (
<>
<HeadSeo
@@ -110,9 +112,13 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
description={
isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : (user.bio as string) || ""
}
name={isDynamicGroup ? dynamicNames.join(", ") : nameOrUsername}
username={isDynamicGroup ? dynamicUsernames.join(", ") : (user.username as string) || ""}
// avatar={user.avatar || undefined}
meeting={{
title: isDynamicGroup ? "" : `${user.bio}`,
profile: { name: `${profile.name}`, image: null },
users: isDynamicGroup
? dynamicUsernames.map((username, index) => ({ username, name: dynamicNames[index] }))
: [{ username: `${user.username}`, name: `${user.name}` }],
}}
/>
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
@@ -143,7 +149,11 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
</div>
)}
<div
className="rounded-md border border-neutral-200 dark:border-neutral-700 dark:hover:border-neutral-600"
className={classNames(
"rounded-md ",
!isEventListEmpty &&
"border border-neutral-200 dark:border-neutral-700 dark:hover:border-neutral-600"
)}
data-testid="event-types">
{user.away ? (
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
@@ -190,16 +200,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
))
)}
</div>
{eventTypes.length === 0 && (
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="p-8 text-center text-gray-400 dark:text-white">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">
{t("uh_oh") as string}
</h2>
<p className="mx-auto max-w-md">{t("no_event_types_have_been_setup") as string}</p>
</div>
</div>
)}
{isEventListEmpty && <EmptyPage name={user.name ?? "User"} />}
</main>
<Toaster position="bottom-right" />
</div>
@@ -283,6 +284,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
if (!users.length) {
return {
notFound: true,
} as {
notFound: true;
};
}
const isDynamicGroup = users.length > 1;
+42 -67
View File
@@ -8,13 +8,15 @@ import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defa
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
import prisma from "@calcom/prisma";
import { User } from "@calcom/prisma/client";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
export type AvailabilityPageProps = inferSSRProps<typeof getStaticProps>;
export type AvailabilityPageProps = inferSSRProps<typeof getStaticProps> & EmbedProps;
export default function Type(props: AvailabilityPageProps) {
const { t } = useLocale();
@@ -77,8 +79,27 @@ async function getUserPageProps(context: GetStaticPropsContext) {
brandColor: true,
darkBrandColor: true,
eventTypes: {
select: { id: true },
// Order by position is important to ensure that the event-type that's enabled is the first in the list because for Free user only first is allowed.
where: {
// Many-to-many relationship causes inclusion of the team events - cool -
// but to prevent these from being selected, make sure the teamId is NULL.
AND: [{ slug }, { teamId: null }],
},
select: {
title: true,
slug: true,
hidden: true,
recurringEvent: true,
length: true,
locations: true,
id: true,
description: true,
price: true,
currency: true,
requiresConfirmation: true,
schedulingType: true,
metadata: true,
seatsPerTimeSlot: true,
},
orderBy: [
{
position: "desc",
@@ -91,57 +112,24 @@ async function getUserPageProps(context: GetStaticPropsContext) {
},
});
if (!user) return { notFound: true };
if (!user || !user.eventTypes) return { notFound: true };
const eventTypes = await prisma.eventType.findMany({
where: {
slug,
OR: [{ userId: user.id }, { users: { some: { id: user.id } } }],
},
// Order is important to ensure that given a slug if there are duplicates, we choose the same event type consistently when showing in event-types list UI(in terms of ordering and disabled event types)
// TODO: If we can ensure that there are no duplicates for a [slug, userId] combination in existing data, this requirement might be avoided.
orderBy: [
{
position: "desc",
},
{
id: "asc",
},
],
select: {
title: true,
slug: true,
hidden: true,
recurringEvent: true,
length: true,
locations: true,
id: true,
description: true,
price: true,
currency: true,
requiresConfirmation: true,
schedulingType: true,
metadata: true,
seatsPerTimeSlot: true,
users: {
select: {
name: true,
username: true,
hideBranding: true,
brandColor: true,
darkBrandColor: true,
theme: true,
plan: true,
allowDynamicBooking: true,
timeZone: true,
const [eventType]: (typeof user.eventTypes[number] & {
users: Pick<User, "name" | "username" | "hideBranding" | "plan" | "timeZone">[];
})[] = [
{
...user.eventTypes[0],
users: [
{
name: user.name,
username: user.username,
hideBranding: user.hideBranding,
plan: user.plan,
timeZone: user.timeZone,
},
},
],
},
});
if (!eventTypes) return { notFound: true };
const [eventType] = eventTypes;
];
if (!eventType) return { notFound: true };
@@ -151,33 +139,20 @@ async function getUserPageProps(context: GetStaticPropsContext) {
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: privacyFilteredLocations(locations),
users: eventType.users.map((user) => ({
name: user.name,
username: user.username,
hideBranding: user.hideBranding,
plan: user.plan,
timeZone: user.timeZone,
})),
});
const profile = eventType.users[0] || user;
return {
props: {
eventType: eventTypeObject,
profile: {
...eventType.users[0],
theme: user.theme,
name: user.name,
username: user.username,
hideBranding: user.hideBranding,
plan: user.plan,
timeZone: user.timeZone,
allowDynamicBooking: false,
weekStart: user.weekStart,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
slug: `${profile.username}/${eventType.slug}`,
image: `${WEBAPP_URL}/${profile.username}/avatar.png`,
slug: `${user.username}/${eventType.slug}`,
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
},
away: user?.away,
isDynamic: false,
+21
View File
@@ -0,0 +1,21 @@
import { GetStaticPropsContext } from "next";
import { getStaticProps as _getStaticProps } from "../[type]";
export { getStaticPaths } from "../[type]";
export { default } from "../[type]";
export const getStaticProps = async (context: GetStaticPropsContext) => {
const staticResponse = await _getStaticProps(context);
if (staticResponse.notFound) {
return staticResponse;
}
return {
...staticResponse,
props: {
...staticResponse.props,
isEmbed: true,
},
};
};
+1
View File
@@ -197,6 +197,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
isDynamicGroupBooking,
hasHashedBookingLink: false,
hashedLink: null,
isEmbed: typeof context.query.embed === "string",
},
};
}
+7
View File
@@ -0,0 +1,7 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[user]";
export { default } from "../[user]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+21 -26
View File
@@ -3,18 +3,13 @@ import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps
type Props = Record<string, unknown> & DocumentProps;
function toRunBeforeReactOnClient() {
const calEmbedMode = location.search.includes("embed=");
try {
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
window.sessionStorage.setItem("calEmbedMode", String(calEmbedMode));
} catch (e) {}
const calEmbedMode =
location.search.includes("embed=") ||
/* Iframe Name */
window.name.includes("cal-embed");
window.isEmbed = () => {
try {
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
return window.sessionStorage.getItem("calEmbedMode") === "true";
} catch (e) {}
// If we can't use sessionStorage to retrieve embed mode, just use the variable. It would fail to detect embed if page in iframe reloads without embed query param in it.
// Once an embed mode always an embed mode
return calEmbedMode;
};
@@ -45,14 +40,14 @@ function toRunBeforeReactOnClient() {
class MyDocument extends Document<Props> {
static async getInitialProps(ctx: DocumentContext) {
const isEmbed = ctx.asPath?.includes("/embed") || ctx.asPath?.includes("embedType=");
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
return { isEmbed, ...initialProps };
}
render() {
const { locale } = this.props.__NEXT_DATA__;
const dir = locale === "ar" || locale === "he" ? "rtl" : "ltr";
return (
<Html lang={locale} dir={dir}>
<Head>
@@ -82,22 +77,22 @@ class MyDocument extends Document<Props> {
/>
</Head>
<body className="dark:bg-darkgray-50 desktop-transparent bg-gray-100">
<body
className="dark:bg-darkgray-50 desktop-transparent bg-gray-100"
style={
this.props.isEmbed
? {
background: "transparent",
// Keep the embed hidden till parent initializes and
// - gives it the appropriate styles if UI instruction is there.
// - gives iframe the appropriate height(equal to document height) which can only be known after loading the page once in browser.
// - Tells iframe which mode it should be in (dark/light) - if there is a a UI instruction for that
visibility: "hidden",
}
: {}
}>
<Main />
<NextScript />
{/* In case of Embed we want background to be transparent so that it merges into the website seamlessly. Also, we keep the body hidden here and embed logic would take care of showing the body when it's ready */}
{/* We are doing it on browser and not on server because there are some pages which are not SSRd */}
<script
dangerouslySetInnerHTML={{
__html: `
if (isEmbed()) {
if(!isPageOptimizedForEmbed()) {
document.body.style.display="none";
}
document.body.style.background="transparent";
}`,
}}
/>
</body>
</Html>
);
+4 -4
View File
@@ -12,6 +12,7 @@ import path from "path";
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies";
@@ -22,7 +23,6 @@ import prisma from "@calcom/prisma";
import { ErrorCode, verifyPassword } from "@lib/auth";
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
import { randomString } from "@lib/random";
import { hostedCal, isSAMLLoginEnabled, samlLoginUrl } from "@lib/saml";
import slugify from "@lib/slugify";
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
@@ -135,7 +135,7 @@ if (isSAMLLoginEnabled) {
version: "2.0",
checks: ["pkce", "state"],
authorization: {
url: `${samlLoginUrl}/api/auth/saml/authorize`,
url: `${WEBAPP_URL}/api/auth/saml/authorize`,
params: {
scope: "",
response_type: "code",
@@ -143,10 +143,10 @@ if (isSAMLLoginEnabled) {
},
},
token: {
url: `${samlLoginUrl}/api/auth/saml/token`,
url: `${WEBAPP_URL}/api/auth/saml/token`,
params: { grant_type: "authorization_code" },
},
userinfo: `${samlLoginUrl}/api/auth/saml/userinfo`,
userinfo: `${WEBAPP_URL}/api/auth/saml/userinfo`,
profile: (profile) => {
return {
id: profile.id || "",
+16 -17
View File
@@ -1,25 +1,24 @@
import { OAuthReqBody } from "@boxyhq/saml-jackson";
import { OAuthReq } from "@boxyhq/saml-jackson";
import { NextApiRequest, NextApiResponse } from "next";
import { HttpError } from "@calcom/lib/http-error";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import jackson from "@lib/jackson";
import { HttpError } from "@lib/core/http/error";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (req.method !== "GET") {
throw new Error("Method not allowed");
}
const { oauthController } = await jackson();
const { oauthController } = await jackson();
const { redirect_url } = await oauthController.authorize(req.query as unknown as OAuthReqBody);
res.redirect(302, redirect_url);
} catch (err: unknown) {
if (err instanceof HttpError) {
console.error("authorize error:", err);
const { message, statusCode = 500 } = err;
return res.status(statusCode).send(message);
}
return res.status(500).send("Unknown error");
if (req.method !== "GET") {
return res.status(400).send("Method not allowed");
}
try {
const { redirect_url } = await oauthController.authorize(req.query as unknown as OAuthReq);
return res.redirect(302, redirect_url as string);
} catch (err) {
const { message, statusCode = 500 } = err as HttpError;
return res.status(statusCode).send(message);
}
}
+10 -21
View File
@@ -1,25 +1,14 @@
import { NextApiRequest, NextApiResponse } from "next";
import { HttpError } from "@calcom/lib/http-error";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import jackson from "@lib/jackson";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (req.method !== "POST") {
throw new Error("Method not allowed");
}
const { oauthController } = await jackson();
const { redirect_url } = await oauthController.samlResponse(req.body);
res.redirect(302, redirect_url);
} catch (err: unknown) {
if (err instanceof HttpError) {
console.error("callback error:", err);
const { message, statusCode = 500 } = err;
return res.status(statusCode).send(message);
}
return res.status(500).send("Unknown error");
}
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { oauthController } = await jackson();
const { redirect_url } = await oauthController.samlResponse(req.body);
if (redirect_url) return res.redirect(302, redirect_url);
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});
+10 -18
View File
@@ -1,21 +1,13 @@
import { NextApiRequest, NextApiResponse } from "next";
import { NextApiRequest } from "next";
import jackson from "@lib/jackson";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (req.method !== "POST") {
throw new Error("Method not allowed");
}
const { oauthController } = await jackson();
const result = await oauthController.token(req.body);
res.json(result);
} catch (err: any) {
console.error("token error:", err);
const { message, statusCode = 500 } = err;
res.status(statusCode).send(message);
}
async function postHandler(req: NextApiRequest) {
const { oauthController } = await jackson();
return await oauthController.token(req.body);
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});
+20 -39
View File
@@ -1,53 +1,34 @@
import { NextApiRequest, NextApiResponse } from "next";
import { NextApiRequest } from "next";
import z from "zod";
import jackson from "@lib/jackson";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
const extractAuthToken = (req: NextApiRequest) => {
const authHeader = req.headers["authorization"];
const parts = (authHeader || "").split(" ");
if (parts.length > 1) {
return parts[1];
}
if (parts.length > 1) return parts[1];
return null;
// check for query param
let arr: string[] = [];
const { access_token } = requestQuery.parse(req.query);
arr = arr.concat(access_token);
if (arr[0].length > 0) return arr[0];
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
};
const requestQuery = z.object({
access_token: z.string(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (req.method !== "GET") {
throw new Error("Method not allowed");
}
const { oauthController } = await jackson();
let token: string | null = extractAuthToken(req);
// check for query param
if (!token) {
let arr: string[] = [];
const { access_token } = requestQuery.parse(req.query);
arr = arr.concat(access_token);
if (arr[0].length > 0) {
token = arr[0];
}
}
if (!token) {
res.status(401).json({ message: "Unauthorized" });
return;
}
const profile = await oauthController.userInfo(token);
res.json(profile);
} catch (err: any) {
console.error("userinfo error:", err);
const { message, statusCode = 500 } = err;
res.status(statusCode).json({ message });
}
async function getHandler(req: NextApiRequest) {
const { oauthController } = await jackson();
const token = extractAuthToken(req);
return await oauthController.userInfo(token);
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
});
@@ -1,294 +0,0 @@
import {
Attendee,
Booking,
BookingReference,
BookingStatus,
EventType,
User,
WebhookTriggerEvents,
} from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import type { TFunction } from "next-i18next";
import { z, ZodError } from "zod";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
import { deleteMeeting } from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
import { sendRequestRescheduleEmail } from "@calcom/emails";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
export type RescheduleResponse = Booking & {
attendees: Attendee[];
};
export type PersonAttendeeCommonFields = Pick<
User,
"id" | "email" | "name" | "locale" | "timeZone" | "username"
>;
const rescheduleSchema = z.object({
bookingId: z.string(),
rescheduleReason: z.string().optional(),
});
const findUserDataByUserId = async (userId: number) => {
return await prisma.user.findUniqueOrThrow({
where: {
id: userId,
},
select: {
id: true,
name: true,
username: true,
email: true,
timeZone: true,
locale: true,
credentials: true,
destinationCalendar: true,
teams: true,
},
});
};
const handler = async (
req: NextApiRequest,
res: NextApiResponse
): Promise<RescheduleResponse | NextApiResponse | void> => {
const session = await getSession({ req });
const {
bookingId,
rescheduleReason: cancellationReason,
}: { bookingId: string; rescheduleReason: string; cancellationReason: string } = req.body;
let user: Awaited<ReturnType<typeof findUserDataByUserId>>;
try {
if (session?.user?.id) {
user = await findUserDataByUserId(session?.user.id);
} else {
return res.status(501).end();
}
const bookingToReschedule = await prisma.booking.findFirstOrThrow({
select: {
id: true,
uid: true,
userId: true,
title: true,
description: true,
startTime: true,
endTime: true,
eventTypeId: true,
eventType: true,
location: true,
attendees: true,
references: true,
customInputs: true,
dynamicEventSlugRef: true,
dynamicGroupSlugRef: true,
destinationCalendar: true,
},
where: {
uid: bookingId,
NOT: {
status: {
in: [BookingStatus.CANCELLED, BookingStatus.REJECTED],
},
},
},
});
if (!bookingToReschedule.userId) {
throw new Error("Booking to reschedule doesn't have an owner.");
}
if (!bookingToReschedule.eventType) {
throw new Error("EventType not found for current booking.");
}
const bookingBelongsToTeam = !!bookingToReschedule.eventType?.teamId;
if (bookingBelongsToTeam && bookingToReschedule.eventType?.teamId) {
const userTeamIds = user.teams.map((item) => item.teamId);
if (userTeamIds.indexOf(bookingToReschedule?.eventType?.teamId) === -1) {
throw new Error("User isn't a member on the team");
}
}
if (!bookingBelongsToTeam && bookingToReschedule.userId !== user.id) {
throw new Error("User isn't owner of the current booking");
}
if (bookingToReschedule && user) {
let event: Partial<EventType> = {};
if (bookingToReschedule.eventTypeId) {
event = await prisma.eventType.findFirstOrThrow({
select: {
title: true,
users: true,
schedulingType: true,
recurringEvent: true,
},
where: {
id: bookingToReschedule.eventTypeId,
},
});
}
await prisma.booking.update({
where: {
id: bookingToReschedule.id,
},
data: {
rescheduled: true,
cancellationReason,
status: BookingStatus.CANCELLED,
updatedAt: dayjs().toISOString(),
},
});
const [mainAttendee] = bookingToReschedule.attendees;
// @NOTE: Should we assume attendees language?
const tAttendees = await getTranslation(mainAttendee.locale ?? "en", "common");
const usersToPeopleType = (
users: PersonAttendeeCommonFields[],
selectedLanguage: TFunction
): Person[] => {
return users?.map((user) => {
return {
email: user.email || "",
name: user.name || "",
username: user?.username || "",
language: { translate: selectedLanguage, locale: user.locale || "en" },
timeZone: user?.timeZone,
};
});
};
const userTranslation = await getTranslation(user.locale ?? "en", "common");
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
const builder = new CalendarEventBuilder();
builder.init({
title: bookingToReschedule.title,
type: event && event.title ? event.title : bookingToReschedule.title,
startTime: bookingToReschedule.startTime.toISOString(),
endTime: bookingToReschedule.endTime.toISOString(),
attendees: usersToPeopleType(
// username field doesn't exists on attendee but could be in the future
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
tAttendees
),
organizer: userAsPeopleType,
});
const director = new CalendarEventDirector();
director.setBuilder(builder);
director.setExistingBooking(bookingToReschedule);
director.setCancellationReason(cancellationReason);
if (event) {
await director.buildForRescheduleEmail();
} else {
await director.buildWithoutEventTypeForRescheduleEmail();
}
// Handling calendar and videos cancellation
// This can set previous time as available, until virtual calendar is done
const credentialsMap = new Map();
user.credentials.forEach((credential) => {
credentialsMap.set(credential.type, credential);
});
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter(
(ref) => !!credentialsMap.get(ref.type)
);
bookingRefsFiltered.forEach((bookingRef) => {
if (bookingRef.uid) {
if (bookingRef.type.endsWith("_calendar")) {
const calendar = getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(
bookingRef.uid,
builder.calendarEvent,
bookingRef.externalCalendarId
);
} else if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
}
}
});
// Send emails
await sendRequestRescheduleEmail(builder.calendarEvent, {
rescheduleLink: builder.rescheduleLink,
});
const evt: CalendarEvent = {
title: bookingToReschedule?.title,
type: event && event.title ? event.title : bookingToReschedule.title,
description: bookingToReschedule?.description || "",
customInputs: isPrismaObjOrUndefined(bookingToReschedule.customInputs),
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
organizer: userAsPeopleType,
attendees: usersToPeopleType(
// username field doesn't exists on attendee but could be in the future
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
tAttendees
),
uid: bookingToReschedule?.uid,
location: bookingToReschedule?.location,
destinationCalendar:
bookingToReschedule?.destinationCalendar || bookingToReschedule?.destinationCalendar,
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
};
// Send webhook
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
// Send Webhook call if hooked to BOOKING.CANCELLED
const subscriberOptions = {
userId: bookingToReschedule.userId,
eventTypeId: (bookingToReschedule.eventTypeId as number) || 0,
triggerEvent: eventTrigger,
};
const webhooks = await getWebhooks(subscriberOptions);
const promises = webhooks.map((webhook) =>
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, evt).catch((e) => {
console.error(
`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`,
e
);
})
);
await Promise.all(promises);
}
return res.status(200).json(bookingToReschedule);
} catch (error) {
if (error instanceof Error) {
throw new Error("Error.request.reschedule " + error?.message);
}
}
};
function validate(
handler: (req: NextApiRequest, res: NextApiResponse) => Promise<RescheduleResponse | NextApiResponse | void>
) {
return async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "POST") {
try {
rescheduleSchema.parse(req.body);
} catch (error) {
if (error instanceof ZodError && error?.name === "ZodError") {
return res.status(400).json(error?.issues);
}
return res.status(402).end();
}
} else {
return res.status(405).end();
}
await handler(req, res);
};
}
export default validate(handler);
+7 -389
View File
@@ -1,396 +1,14 @@
import {
BookingStatus,
Prisma,
PrismaPromise,
WebhookTriggerEvents,
WorkflowMethods,
WorkflowReminder,
} from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import type { NextApiRequest } from "next";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { DailyLocationType } from "@calcom/app-store/locations";
import { refund } from "@calcom/app-store/stripepayment/lib/server";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { deleteMeeting } from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmails } from "@calcom/emails";
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import sendPayload, { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
import { getSession } from "@calcom/lib/auth";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { defaultResponder, defaultHandler } from "@calcom/lib/server";
import { getTranslation } from "@server/lib/i18n";
const bodySchema = z.object({
uid: z.string(),
allRemainingBookings: z.boolean().optional(),
cancellationReason: z.string().optional(),
});
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { uid, allRemainingBookings, cancellationReason } = bodySchema.parse(req.body);
async function handler(req: NextApiRequest & { userId?: number }) {
const session = await getSession({ req });
const bookingToDelete = await prisma.booking.findUnique({
where: {
uid,
},
select: {
...bookingMinimalSelect,
recurringEventId: true,
userId: true,
user: {
select: {
id: true,
credentials: true,
email: true,
timeZone: true,
name: true,
destinationCalendar: true,
},
},
location: true,
references: {
select: {
uid: true,
type: true,
externalCalendarId: true,
credentialId: true,
},
},
payment: true,
paid: true,
eventType: {
select: {
recurringEvent: true,
title: true,
description: true,
requiresConfirmation: true,
price: true,
currency: true,
length: true,
workflows: {
include: {
workflow: {
include: {
steps: true,
},
},
},
},
},
},
uid: true,
eventTypeId: true,
destinationCalendar: true,
smsReminderNumber: true,
workflowReminders: true,
scheduledJobs: true,
},
});
if (!bookingToDelete || !bookingToDelete.user) {
throw new HttpError({ statusCode: 404, message: "Booking not found" });
}
if ((!session || session.user?.id !== bookingToDelete.user?.id) && bookingToDelete.startTime < new Date()) {
throw new HttpError({ statusCode: 403, message: "Cannot cancel past events" });
}
if (!bookingToDelete.userId) {
throw new HttpError({ statusCode: 404, message: "User not found" });
}
const organizer = await prisma.user.findFirstOrThrow({
where: {
id: bookingToDelete.userId,
},
select: {
name: true,
email: true,
timeZone: true,
locale: true,
},
});
const attendeesListPromises = bookingToDelete.attendees.map(async (attendee) => {
return {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
};
});
const attendeesList = await Promise.all(attendeesListPromises);
const tOrganizer = await getTranslation(organizer.locale ?? "en", "common");
const evt: CalendarEvent = {
title: bookingToDelete?.title,
type: (bookingToDelete?.eventType?.title as string) || bookingToDelete?.title,
description: bookingToDelete?.description || "",
customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs),
startTime: bookingToDelete?.startTime ? dayjs(bookingToDelete.startTime).format() : "",
endTime: bookingToDelete?.endTime ? dayjs(bookingToDelete.endTime).format() : "",
organizer: {
email: organizer.email,
name: organizer.name ?? "Nameless",
timeZone: organizer.timeZone,
language: { translate: tOrganizer, locale: organizer.locale ?? "en" },
},
attendees: attendeesList,
uid: bookingToDelete?.uid,
/* Include recurringEvent information only when cancelling all bookings */
recurringEvent: allRemainingBookings
? parseRecurringEvent(bookingToDelete.eventType?.recurringEvent)
: undefined,
location: bookingToDelete?.location,
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
cancellationReason: cancellationReason,
};
// Hook up the webhook logic here
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
// Send Webhook call if hooked to BOOKING.CANCELLED
const subscriberOptions = {
userId: bookingToDelete.userId,
eventTypeId: (bookingToDelete.eventTypeId as number) || 0,
triggerEvent: eventTrigger,
};
const eventTypeInfo: EventTypeInfo = {
eventTitle: bookingToDelete?.eventType?.title || null,
eventDescription: bookingToDelete?.eventType?.description || null,
requiresConfirmation: bookingToDelete?.eventType?.requiresConfirmation || null,
price: bookingToDelete?.eventType?.price || null,
currency: bookingToDelete?.eventType?.currency || null,
length: bookingToDelete?.eventType?.length || null,
};
const webhooks = await getWebhooks(subscriberOptions);
const promises = webhooks.map((webhook) =>
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
...evt,
...eventTypeInfo,
status: "CANCELLED",
}).catch((e) => {
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`, e);
})
);
await Promise.all(promises);
//Workflows - schedule reminders
if (bookingToDelete.eventType?.workflows) {
await sendCancelledReminders(
bookingToDelete.eventType?.workflows,
bookingToDelete.smsReminderNumber,
evt
);
}
let updatedBookings: {
uid: string;
workflowReminders: WorkflowReminder[];
scheduledJobs: string[];
}[] = [];
// by cancelling first, and blocking whilst doing so; we can ensure a cancel
// action always succeeds even if subsequent integrations fail cancellation.
if (bookingToDelete.eventType?.recurringEvent && bookingToDelete.recurringEventId && allRemainingBookings) {
const recurringEventId = bookingToDelete.recurringEventId;
// Proceed to mark as cancelled all remaining recurring events instances (greater than or equal to right now)
await prisma.booking.updateMany({
where: {
recurringEventId,
startTime: {
gte: new Date(),
},
},
data: {
status: BookingStatus.CANCELLED,
cancellationReason: cancellationReason,
},
});
const allUpdatedBookings = await prisma.booking.findMany({
where: {
recurringEventId: bookingToDelete.recurringEventId,
startTime: {
gte: new Date(),
},
},
select: {
workflowReminders: true,
uid: true,
scheduledJobs: true,
},
});
updatedBookings = updatedBookings.concat(allUpdatedBookings);
} else {
const updatedBooking = await prisma.booking.update({
where: {
uid,
},
data: {
status: BookingStatus.CANCELLED,
cancellationReason: cancellationReason,
},
select: {
workflowReminders: true,
uid: true,
scheduledJobs: true,
},
});
updatedBookings.push(updatedBooking);
}
/** TODO: Remove this without breaking functionality */
if (bookingToDelete.location === DailyLocationType) {
bookingToDelete.user.credentials.push(FAKE_DAILY_CREDENTIAL);
}
const apiDeletes = [];
const bookingCalendarReference = bookingToDelete.references.find((reference) =>
reference.type.includes("_calendar")
);
if (bookingCalendarReference) {
const { credentialId, uid, externalCalendarId } = bookingCalendarReference;
// If the booking calendar reference contains a credentialId
if (credentialId) {
// Find the correct calendar credential under user credentials
const calendarCredential = bookingToDelete.user.credentials.find(
(credential) => credential.id === credentialId
);
if (calendarCredential) {
const calendar = getCalendar(calendarCredential);
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId));
}
// For bookings made before the refactor we go through the old behaviour of running through each calendar credential
} else {
bookingToDelete.user.credentials
.filter((credential) => credential.type.endsWith("_calendar"))
.forEach((credential) => {
const calendar = getCalendar(credential);
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId));
});
}
}
const bookingVideoReference = bookingToDelete.references.find((reference) =>
reference.type.includes("_video")
);
// If the video reference has a credentialId find the specific credential
if (bookingVideoReference && bookingVideoReference.credentialId) {
const { credentialId, uid } = bookingVideoReference;
if (credentialId) {
const videoCredential = bookingToDelete.user.credentials.find(
(credential) => credential.id === credentialId
);
if (videoCredential) {
apiDeletes.push(deleteMeeting(videoCredential, uid));
}
}
// For bookings made before this refactor we go through the old behaviour of running through each video credential
} else {
bookingToDelete.user.credentials
.filter((credential) => credential.type.endsWith("_video"))
.forEach((credential) => {
apiDeletes.push(deleteMeeting(credential, uid));
});
}
// Avoiding taking care of recurrence for now as Payments are not supported with Recurring Events at the moment
if (bookingToDelete && bookingToDelete.paid) {
const evt: CalendarEvent = {
type: bookingToDelete?.eventType?.title as string,
title: bookingToDelete.title,
description: bookingToDelete.description ?? "",
customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs),
startTime: bookingToDelete.startTime.toISOString(),
endTime: bookingToDelete.endTime.toISOString(),
organizer: {
email: bookingToDelete.user?.email ?? "dev@calendso.com",
name: bookingToDelete.user?.name ?? "no user",
timeZone: bookingToDelete.user?.timeZone ?? "",
language: { translate: tOrganizer, locale: organizer.locale ?? "en" },
},
attendees: attendeesList,
location: bookingToDelete.location ?? "",
uid: bookingToDelete.uid ?? "",
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
};
await refund(bookingToDelete, evt);
await prisma.booking.update({
where: {
id: bookingToDelete.id,
},
data: {
status: BookingStatus.REJECTED,
},
});
// We skip the deletion of the event, because that would also delete the payment reference, which we should keep
await apiDeletes;
return res.status(200).json({ message: "Booking successfully deleted." });
}
const attendeeDeletes = prisma.attendee.deleteMany({
where: {
bookingId: bookingToDelete.id,
},
});
const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
where: {
bookingId: bookingToDelete.id,
},
});
// delete scheduled jobs of cancelled bookings
updatedBookings.forEach((booking) => {
cancelScheduledJobs(booking);
});
//Workflows - delete all reminders for bookings
const remindersToDelete: PrismaPromise<Prisma.BatchPayload>[] = [];
updatedBookings.forEach((booking) => {
booking.workflowReminders.forEach((reminder) => {
if (reminder.scheduled && reminder.referenceId) {
if (reminder.method === WorkflowMethods.EMAIL) {
deleteScheduledEmailReminder(reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.referenceId);
}
}
const reminderToDelete = prisma.workflowReminder.deleteMany({
where: {
id: reminder.id,
},
});
remindersToDelete.push(reminderToDelete);
});
});
await Promise.all([apiDeletes, attendeeDeletes, bookingReferenceDeletes].concat(remindersToDelete));
await sendCancelledEmails(evt);
res.status(204).end();
/* To mimic API behavior */
req.userId = session?.user?.id;
return await handleCancelBooking(req);
}
export default defaultHandler({
+13 -1
View File
@@ -32,6 +32,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
timeZone: "America/Chihuahua",
language,
},
{
email: "pro@example.com",
name: "pro@example.com",
timeZone: "America/Chihuahua",
language,
},
{
email: "pro@example.com",
name: "pro@example.com",
timeZone: "America/Chihuahua",
language,
},
],
location: "Zoom video",
destinationCalendar: null,
@@ -48,7 +60,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write(
renderEmail("OrganizerRequestReminderEmail", {
renderEmail("AttendeeScheduledEmail", {
attendee: evt.attendees[0],
calEvent: evt,
})
+108
View File
@@ -0,0 +1,108 @@
import { ImageResponse } from "@vercel/og";
import { NextApiRequest } from "next";
import type { SatoriOptions } from "satori";
import { z } from "zod";
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
const calFont = fetch(new URL("../../../../public/fonts/cal.ttf", import.meta.url)).then((res) =>
res.arrayBuffer()
);
const interFont = fetch(new URL("../../../../public/fonts/Inter-Regular.ttf", import.meta.url)).then((res) =>
res.arrayBuffer()
);
const interFontMedium = fetch(new URL("../../../../public/fonts/Inter-Medium.ttf", import.meta.url)).then(
(res) => res.arrayBuffer()
);
export const config = {
runtime: "experimental-edge",
};
const meetingSchema = z.object({
imageType: z.literal("meeting"),
title: z.string(),
names: z.string().array(),
usernames: z.string().array(),
meetingProfileName: z.string(),
meetingImage: z.string().nullable().optional(),
});
const appSchema = z.object({
imageType: z.literal("app"),
name: z.string(),
description: z.string(),
slug: z.string(),
});
const genericSchema = z.object({
imageType: z.literal("generic"),
title: z.string(),
description: z.string(),
});
export default async function handler(req: NextApiRequest) {
const { searchParams } = new URL(`${req.url}`);
const imageType = searchParams.get("type");
const [calFontData, interFontData, interFontMediumData] = await Promise.all([
calFont,
interFont,
interFontMedium,
]);
const ogConfig = {
fonts: [
{ name: "inter", data: interFontData, weight: 400 },
{ name: "inter", data: interFontMediumData, weight: 500 },
{ name: "cal", data: calFontData, weight: 400 },
{ name: "cal", data: calFontData, weight: 600 },
] as SatoriOptions["fonts"],
};
switch (imageType) {
case "meeting": {
const { names, usernames, title, meetingProfileName, meetingImage } = meetingSchema.parse({
names: searchParams.getAll("names"),
usernames: searchParams.getAll("usernames"),
title: searchParams.get("title"),
meetingProfileName: searchParams.get("meetingProfileName"),
meetingImage: searchParams.get("meetingImage"),
imageType,
});
return new ImageResponse(
(
<Meeting
title={title}
profile={{ name: meetingProfileName, image: meetingImage }}
users={names.map((name, index) => ({ name, username: usernames[index] }))}
/>
),
ogConfig
);
}
case "app": {
const { name, description, slug } = appSchema.parse({
name: searchParams.get("name"),
description: searchParams.get("description"),
slug: searchParams.get("slug"),
imageType,
});
return new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
}
case "generic": {
const { title, description } = genericSchema.parse({
title: searchParams.get("title"),
description: searchParams.get("description"),
imageType,
});
return new ImageResponse(<Generic title={title} description={description} />, ogConfig);
}
default:
return new Response("What you're looking for is not here..", { status: 404 });
}
}
+10 -1
View File
@@ -1,6 +1,7 @@
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
import prisma from "@calcom/prisma";
@@ -11,6 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const username = req.query.username as string;
const teamname = req.query.teamname as string;
let identity;
let linksToThisRoute = false;
if (username) {
const user = await prisma.user.findUnique({
where: {
@@ -26,6 +28,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
email: user?.email,
avatar: user?.avatar,
};
linksToThisRoute =
identity.avatar === `${CAL_URL}/${username}/avatar.png` ||
identity.avatar === `${WEBAPP_URL}/${username}/avatar.png`;
} else if (teamname) {
const team = await prisma.team.findUnique({
where: {
@@ -40,6 +45,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
shouldDefaultBeNameBased: true,
avatar: team?.logo,
};
linksToThisRoute =
identity.avatar === `${CAL_URL}/team/${teamname}/avatar.png` ||
identity.avatar === `${WEBAPP_URL}/team/${teamname}/avatar.png`;
}
const emailMd5 = crypto
@@ -47,7 +55,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
.update((identity?.email as string) || "guest@example.com")
.digest("hex");
const img = identity?.avatar;
if (!img) {
// If image isn't set or links to this route itself, use default avatar
if (!img || linksToThisRoute) {
let defaultSrc = defaultAvatarSrc({ md5: emailMd5 });
if (identity?.shouldDefaultBeNameBased) {
defaultSrc = getPlaceholderAvatar(null, identity.name);
+2 -1
View File
@@ -12,7 +12,7 @@ import prisma from "@calcom/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import App from "@components/v2/apps/App";
import App from "@components/apps/App";
const components = {
a: ({ href = "", ...otherProps }: JSX.IntrinsicElements["a"]) => (
@@ -34,6 +34,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
return (
<App
name={data.name}
description={data.description}
isGlobal={data.isGlobal}
slug={data.slug}
variant={data.variant}
+2 -2
View File
@@ -2,8 +2,8 @@ import { GetStaticPaths, InferGetStaticPropsType } from "next";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { AppSetupPage } from "@calcom/app-store/_pages/setup";
import { getStaticProps } from "@calcom/app-store/_pages/setup/_getStaticProps";
import { AppSetupPage } from "@calcom/app-store/_pages/v2/setup";
export default function SetupInformation(props: InferGetStaticPropsType<typeof getStaticProps>) {
const router = useRouter();
@@ -23,7 +23,7 @@ export default function SetupInformation(props: InferGetStaticPropsType<typeof g
});
}
return <AppSetupPage slug={`${slug}-V2`} {...props} />;
return <AppSetupPage slug={slug} {...props} />;
}
export const getStaticPaths: GetStaticPaths = async () => {
+5
View File
@@ -9,6 +9,8 @@ import AppStoreCategories from "@calcom/ui/v2/core/apps/Categories";
import TrendingAppsSlider from "@calcom/ui/v2/core/apps/TrendingAppsSlider";
import AppsLayout from "@calcom/ui/v2/core/layouts/AppsLayout";
import { ssgInit } from "@server/lib/ssg";
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getServerSideProps>) {
const { t } = useLocale();
@@ -22,6 +24,8 @@ export default function Apps({ appStore, categories }: InferGetStaticPropsType<t
}
export const getServerSideProps = async (context: NextPageContext) => {
const ssg = await ssgInit(context);
const session = await getSession(context);
let appStore;
@@ -42,6 +46,7 @@ export const getServerSideProps = async (context: NextPageContext) => {
}, {} as Record<string, number>);
return {
props: {
trpcState: ssg.dehydrate(),
categories: Object.entries(categories)
.map(([name, count]): { name: AppCategories; count: number } => ({
name: name as AppCategories,
+42 -33
View File
@@ -1,4 +1,4 @@
import { InferGetServerSidePropsType } from "next";
import { useRouter } from "next/router";
import z from "zod";
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
@@ -9,7 +9,6 @@ import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { App } from "@calcom/types/App";
import { AppGetServerSidePropsContext } from "@calcom/types/AppGetServerSideProps";
import { Icon } from "@calcom/ui/Icon";
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
import { Alert } from "@calcom/ui/v2/core/Alert";
import Button from "@calcom/ui/v2/core/Button";
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
@@ -20,18 +19,21 @@ import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/Disconnect
import { QueryCell } from "@lib/QueryCell";
import { CalendarListContainer } from "@components/v2/apps/CalendarListContainer";
import IntegrationListItem from "@components/v2/apps/IntegrationListItem";
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
import IntegrationListItem from "@components/apps/IntegrationListItem";
import SkeletonLoader from "@components/v2/availability/SkeletonLoader";
function ConnectOrDisconnectIntegrationButton(props: {
credentialIds: number[];
type: App["type"];
isGlobal?: boolean;
installed?: boolean;
invalidCredentialIds?: number[];
}) {
const { type, credentialIds, isGlobal, installed, invalidCredentialIds } = props;
const { t } = useLocale();
const [credentialId] = props.credentialIds;
const type = props.type;
const [credentialId] = credentialIds;
const utils = trpc.useContext();
const handleOpenChange = () => {
utils.invalidateQueries(["viewer.integrations"]);
@@ -48,6 +50,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
/>
);
}
return (
<DisconnectIntegration
credentialId={credentialId}
@@ -57,7 +60,8 @@ function ConnectOrDisconnectIntegrationButton(props: {
/>
);
}
if (!props.installed) {
if (!installed) {
return (
<div className="flex items-center truncate">
<Alert severity="warning" title={t("not_installed")} />
@@ -65,7 +69,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
);
}
/** We don't need to "Connect", just show that it's installed */
if (props.isGlobal) {
if (isGlobal) {
return (
<div className="truncate px-3 py-2">
<h3 className="text-sm font-medium text-gray-700">{t("default")}</h3>
@@ -74,7 +78,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
}
return (
<InstallAppButton
type={props.type}
type={type}
render={(buttonProps) => (
<Button color="secondary" {...buttonProps} data-testid="integration-connection-button">
{t("install")}
@@ -98,28 +102,32 @@ interface IntegrationsListProps {
const IntegrationsList = ({ data }: IntegrationsListProps) => {
return (
<List className="flex flex-col gap-6" noBorderTreatment>
{data.items.map((item) => (
<IntegrationListItem
name={item.name}
slug={item.slug}
key={item.title}
title={item.title}
logo={item.logo}
description={item.description}
separate={true}
actions={
<div className="flex w-16 justify-end">
<ConnectOrDisconnectIntegrationButton
credentialIds={item.credentialIds}
type={item.type}
isGlobal={item.isGlobal}
installed
/>
</div>
}>
<AppSettings slug={item.slug} />
</IntegrationListItem>
))}
{data.items
.filter((item) => item.invalidCredentialIds)
.map((item) => (
<IntegrationListItem
name={item.name}
slug={item.slug}
key={item.title}
title={item.title}
logo={item.logo}
description={item.description}
separate={true}
invalidCredential={item.invalidCredentialIds.length > 0}
actions={
<div className="flex w-16 justify-end">
<ConnectOrDisconnectIntegrationButton
credentialIds={item.credentialIds}
type={item.type}
isGlobal={item.isGlobal}
installed
invalidCredentialIds={item.invalidCredentialIds}
/>
</div>
}>
<AppSettings slug={item.slug} />
</IntegrationListItem>
))}
</List>
);
};
@@ -188,9 +196,10 @@ const querySchema = z.object({
category: z.nativeEnum(InstalledAppVariants),
});
export default function InstalledApps({ category }: InferGetServerSidePropsType<typeof getServerSideProps>) {
export default function InstalledApps() {
const { t } = useLocale();
const router = useRouter();
const category = router.query.category;
return (
<InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
{(category === InstalledAppVariants.payment || category === InstalledAppVariants.conferencing) && (
+93 -97
View File
@@ -4,21 +4,21 @@ import { getCsrfToken, signIn } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useForm, FormProvider } from "react-hook-form";
import { FaGoogle } from "react-icons/fa";
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { Icon } from "@calcom/ui";
import { Alert } from "@calcom/ui/Alert";
import { Button, EmailField, Form, PasswordField } from "@calcom/ui/v2";
import { Button, EmailField, PasswordField } from "@calcom/ui/v2";
import SAMLLogin from "@calcom/ui/v2/modules/auth/SAMLLogin";
import { ErrorCode, getSession } from "@lib/auth";
import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants";
import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@lib/saml";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import AddToHomescreen from "@components/AddToHomescreen";
@@ -39,15 +39,14 @@ export default function Login({
csrfToken,
isGoogleLoginEnabled,
isSAMLLoginEnabled,
hostedCal,
samlTenantID,
samlProductID,
}: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const router = useRouter();
const form = useForm<LoginValues>();
const { formState } = form;
const { isSubmitting } = formState;
const methods = useForm<LoginValues>();
const { register, formState } = methods;
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
@@ -86,7 +85,7 @@ export default function Login({
<Button
onClick={() => {
setTwoFactorRequired(false);
form.setValue("totpCode", "");
methods.setValue("totpCode", "");
}}
StartIcon={Icon.FiArrowLeft}
color="minimal">
@@ -94,6 +93,23 @@ export default function Login({
</Button>
);
const onSubmit = async (values: LoginValues) => {
setErrorMessage(null);
telemetry.event(telemetryEventTypes.login, collectPageParameters());
const res = await signIn<"credentials">("credentials", {
...values,
callbackUrl,
redirect: false,
});
if (!res) setErrorMessage(errorMessages[ErrorCode.InternalServerError]);
// we're logged in! let's do a hard refresh to the desired url
else if (!res.error) router.push(callbackUrl);
// reveal two factor input if required
else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true);
// fallback if error not found
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
};
return (
<>
<AuthContainer
@@ -102,100 +118,81 @@ export default function Login({
showLogo
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
footerText={twoFactorRequired ? TwoFactorFooter : LoginFooter}>
<Form
form={form}
handleSubmit={async (values) => {
setErrorMessage(null);
telemetry.event(telemetryEventTypes.login, collectPageParameters());
const res = await signIn<"credentials">("credentials", {
...values,
callbackUrl,
redirect: false,
});
if (!res) setErrorMessage(errorMessages[ErrorCode.InternalServerError]);
// we're logged in! let's do a hard refresh to the desired url
else if (!res.error) router.push(callbackUrl);
// reveal two factor input if required
else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true);
// fallback if error not found
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
}}
data-testid="login-form">
<div>
<input
defaultValue={csrfToken || undefined}
type="hidden"
hidden
{...form.register("csrfToken")}
/>
</div>
<div className="space-y-6">
<div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
<EmailField
id="email"
label={t("email_address")}
defaultValue={router.query.email as string}
placeholder="john.doe@example.com"
required
{...form.register("email")}
/>
<div className="relative">
<div className="absolute right-0 -top-[6px] z-10">
<Link href="/auth/forgot-password">
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
{t("forgot")}
</a>
</Link>
</div>
<PasswordField
id="password"
autoComplete="current-password"
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} data-testid="login-form">
<div>
<input defaultValue={csrfToken || undefined} type="hidden" hidden {...register("csrfToken")} />
</div>
<div className="space-y-6">
<div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
<EmailField
id="email"
label={t("email_address")}
defaultValue={router.query.email as string}
placeholder="john.doe@example.com"
required
className="mb-0"
{...form.register("password")}
{...register("email")}
/>
<div className="relative">
<div className="absolute right-0 -top-[6px] z-10">
<Link href="/auth/forgot-password">
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
{t("forgot")}
</a>
</Link>
</div>
<PasswordField
id="password"
autoComplete="current-password"
required
className="mb-0"
{...register("password")}
/>
</div>
</div>
</div>
{twoFactorRequired && <TwoFactor center />}
{twoFactorRequired && <TwoFactor center />}
{errorMessage && <Alert severity="error" title={errorMessage} />}
<Button type="submit" color="primary" disabled={isSubmitting} className="w-full justify-center">
{twoFactorRequired ? t("submit") : t("sign_in")}
</Button>
</div>
</Form>
{!twoFactorRequired && (
<>
{(isGoogleLoginEnabled || isSAMLLoginEnabled) && <hr className="my-8" />}
<div className="space-y-3">
{isGoogleLoginEnabled && (
<Button
color="secondary"
className="w-full justify-center"
data-testid="google"
StartIcon={FaGoogle}
onClick={async (e) => {
e.preventDefault();
// track Google logins. Without personal data/payload
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
await signIn("google");
}}>
{t("signin_with_google")}
</Button>
)}
{isSAMLLoginEnabled && (
<SAMLLogin
email={form.getValues("email")}
samlTenantID={samlTenantID}
samlProductID={samlProductID}
hostedCal={hostedCal}
setErrorMessage={setErrorMessage}
/>
)}
{errorMessage && <Alert severity="error" title={errorMessage} />}
<Button
type="submit"
color="primary"
disabled={formState.isSubmitting}
className="w-full justify-center">
{twoFactorRequired ? t("submit") : t("sign_in")}
</Button>
</div>
</>
)}
</form>
{!twoFactorRequired && (
<>
{(isGoogleLoginEnabled || isSAMLLoginEnabled) && <hr className="my-8" />}
<div className="space-y-3">
{isGoogleLoginEnabled && (
<Button
color="secondary"
className="w-full justify-center"
data-testid="google"
StartIcon={FaGoogle}
onClick={async (e) => {
e.preventDefault();
// track Google logins. Without personal data/payload
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
await signIn("google");
}}>
{t("signin_with_google")}
</Button>
)}
{isSAMLLoginEnabled && (
<SAMLLogin
samlTenantID={samlTenantID}
samlProductID={samlProductID}
setErrorMessage={setErrorMessage}
/>
)}
</div>
</>
)}
</FormProvider>
</AuthContainer>
<AddToHomescreen />
</>
@@ -233,7 +230,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
trpcState: ssr.dehydrate(),
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
isSAMLLoginEnabled,
hostedCal,
samlTenantID,
samlProductID,
},
+7 -1
View File
@@ -5,12 +5,18 @@ import { useEffect } from "react";
import { getPremiumPlanPrice } from "@calcom/app-store/stripepayment/lib/utils";
import stripe from "@calcom/features/ee/payments/server/stripe";
import {
hostedCal,
isSAMLLoginEnabled,
samlProductID,
samlTenantID,
samlTenantProduct,
} from "@calcom/features/ee/sso/lib/saml";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import prisma from "@calcom/prisma";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID, samlTenantProduct } from "@lib/saml";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { ssrInit } from "@server/lib/ssr";
+1 -1
View File
@@ -7,9 +7,9 @@ import { useEffect, useState, useRef } from "react";
import z from "zod";
import { WEBAPP_URL } from "@calcom/lib/constants";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/v2/";
import showToast from "@calcom/ui/v2/core/notifications";
import Loader from "@components/Loader";
+1 -1
View File
@@ -162,7 +162,7 @@ export default function Availability({ schedule }: { schedule: number }) {
</div>
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="xl:max-w-80 w-full pr-4 sm:p-0">
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
+3 -6
View File
@@ -1,5 +1,4 @@
import autoAnimate from "@formkit/auto-animate";
import { useRef, useEffect } from "react";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -16,7 +15,6 @@ import SkeletonLoader from "@components/v2/availability/SkeletonLoader";
export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) {
const { t } = useLocale();
const utils = trpc.useContext();
const animationParentRef = useRef(null);
const meQuery = trpc.useQuery(["viewer.me"]);
@@ -50,9 +48,8 @@ export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availab
});
// Adds smooth delete button - item fades and old item slides into place
useEffect(() => {
animationParentRef.current && autoAnimate(animationParentRef.current);
}, [animationParentRef]);
const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
return (
<>
+4 -6
View File
@@ -1,7 +1,7 @@
import autoAnimate from "@formkit/auto-animate";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { GetStaticPaths, GetStaticProps } from "next";
import { useRouter } from "next/router";
import { Fragment, useEffect, useRef } from "react";
import { Fragment } from "react";
import { z } from "zod";
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
@@ -46,7 +46,7 @@ export default function Bookings() {
});
// Animate page (tab) tranistions to look smoothing
const animationParentRef = useRef(null);
const buttonInView = useInViewObserver(() => {
if (!query.isFetching && query.hasNextPage && query.status === "success") {
query.fetchNextPage();
@@ -73,9 +73,7 @@ export default function Bookings() {
return true;
};
useEffect(() => {
animationParentRef.current && autoAnimate(animationParentRef.current);
}, [animationParentRef]);
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
return (
<BookingLayout heading={t("bookings")} subtitle={t("bookings_description")}>
+3 -3
View File
@@ -14,8 +14,8 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { Button } from "@calcom/ui/Button";
import { Icon } from "@calcom/ui/Icon";
import { Button } from "@calcom/ui/v2/core/Button";
import { getSession } from "@lib/auth";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -51,7 +51,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
<CustomBranding lightVal={props.profile?.brandColor} darkVal={props.profile?.darkBrandColor} />
<main className="h-full sm:flex sm:items-center">
<div className="mx-auto flex justify-center px-4 pt-4 pb-20 sm:block sm:p-0">
<div className="inline-block transform overflow-hidden rounded-md border bg-white px-8 pt-5 pb-4 text-left align-bottom transition-all dark:border-neutral-700 dark:bg-gray-800 sm:my-8 sm:w-full sm:max-w-lg sm:py-6 sm:align-middle">
<div className="inline-block transform overflow-hidden rounded-md border bg-white px-8 pt-5 pb-4 text-left align-bottom transition-all dark:border-neutral-700 dark:bg-gray-800 sm:my-8 sm:w-full sm:max-w-lg sm:py-6 sm:align-middle">
<div>
<div>
{error && (
@@ -178,7 +178,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
const payload = {
uid: uid,
reason: cancellationReason,
cancellationReason: cancellationReason,
allRemainingBookings: !!props.recurringInstances,
};
+8 -1
View File
@@ -10,12 +10,13 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { getWorkingHours } from "@lib/availability";
import { GetBookingType } from "@lib/getBooking";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
import { ssrInit } from "@server/lib/ssr";
export type DynamicAvailabilityPageProps = inferSSRProps<typeof getServerSideProps>;
export type DynamicAvailabilityPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export default function Type(props: DynamicAvailabilityPageProps) {
return <AvailabilityPage {...props} />;
@@ -48,11 +49,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
if (!userId)
return {
notFound: true,
} as {
notFound: true;
};
if (hashedLink?.eventType.slug !== slug)
return {
notFound: true,
} as {
notFound: true;
};
const users = await prisma.user.findMany({
@@ -92,6 +97,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
if (!users || !users.length) {
return {
notFound: true,
} as {
notFound: true;
};
}
+7
View File
@@ -0,0 +1,7 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[slug]";
export { default } from "../[slug]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+1 -1
View File
@@ -2,7 +2,6 @@ import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { parseRecurringEvent } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { bookEventTypeSelect } from "@calcom/prisma/selects";
@@ -124,6 +123,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
isDynamicGroupBooking: false,
hasHashedBookingLink: true,
hashedLink: link,
isEmbed: typeof context.query.embed === "string",
},
};
}
+9 -7
View File
@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import autoAnimate from "@formkit/auto-animate";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -69,6 +69,7 @@ export type FormValues = {
periodCountCalendarDays: "1" | "0";
periodDates: { startDate: Date; endDate: Date };
seatsPerTimeSlot: number | null;
seatsShowAttendees: boolean | null;
seatsPerTimeSlotEnabled: boolean;
minimumBookingNotice: number;
beforeBufferTime: number;
@@ -105,13 +106,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
// TODO: It isn't a good idea to maintain state using setEventType. If we want to connect the SSR'd data to tRPC, we should useQuery(["viewer.eventTypes.get"]) with initialData
// Due to this change, when Form is saved, there is no way to propagate that info to eventType (e.g. disabling stripe app doesn't allow recurring tab to be enabled without refresh).
const [eventType, setEventType] = useState(dbEventType);
const animationParentRef = useRef(null);
const router = useRouter();
const { tabName } = querySchema.parse(router.query);
useEffect(() => {
animationParentRef.current && autoAnimate(animationParentRef.current);
}, [animationParentRef]);
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
const updateMutation = trpc.useMutation("viewer.eventTypes.update", {
onSuccess: async ({ eventType: newEventType }) => {
@@ -232,6 +231,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
beforeBufferTime,
afterBufferTime,
seatsPerTimeSlot,
seatsShowAttendees,
bookingLimits,
recurringEvent,
locations,
@@ -259,6 +259,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
afterEventBuffer: afterBufferTime,
bookingLimits,
seatsPerTimeSlot,
seatsShowAttendees,
metadata,
});
}}>
@@ -391,6 +392,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
price: true,
destinationCalendar: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
workflows: {
include: {
workflow: {
@@ -458,7 +460,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const eventType = {
...restEventType,
schedule: rawEventType.schedule?.id || rawEventType.users[0].defaultScheduleId,
schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null,
recurringEvent: parseRecurringEvent(restEventType.recurringEvent),
bookingLimits: parseBookingLimit(restEventType.bookingLimits),
locations: locations as unknown as LocationObject[],
+55 -10
View File
@@ -98,13 +98,34 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
});
const setHiddenMutation = trpc.useMutation("viewer.eventTypes.update", {
onError: async (err) => {
console.error(err.message);
onMutate: async ({ id }) => {
await utils.cancelQuery(["viewer.eventTypes"]);
await utils.invalidateQueries(["viewer.eventTypes"]);
const previousValue = utils.getQueryData(["viewer.eventTypes"]);
if (previousValue) {
const newList = [...types];
const itemIndex = newList.findIndex((item) => item.id === id);
if (itemIndex !== -1 && newList[itemIndex]) {
newList[itemIndex].hidden = !newList[itemIndex].hidden;
}
utils.setQueryData(["viewer.eventTypes"], {
...previousValue,
eventTypeGroups: [
...previousValue.eventTypeGroups.slice(0, groupIndex),
{ ...group, eventTypes: newList },
...previousValue.eventTypeGroups.slice(groupIndex + 1),
],
});
}
return { previousValue };
},
onSettled: async () => {
await utils.invalidateQueries(["viewer.eventTypes"]);
onError: async (err, _, context) => {
if (context?.previousValue) {
utils.setQueryData(["viewer.eventTypes"], context.previousValue);
}
console.error(err.message);
},
onSettled: () => {
utils.invalidateQueries(["viewer.eventTypes"]);
},
});
@@ -169,12 +190,31 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
};
const deleteMutation = trpc.useMutation("viewer.eventTypes.delete", {
onSuccess: async () => {
await utils.invalidateQueries(["viewer.eventTypes"]);
onSuccess: () => {
showToast(t("event_type_deleted_successfully"), "success");
setDeleteDialogOpen(false);
},
onError: (err) => {
onMutate: async ({ id }) => {
await utils.cancelQuery(["viewer.eventTypes"]);
const previousValue = utils.getQueryData(["viewer.eventTypes"]);
if (previousValue) {
const newList = types.filter((item) => item.id !== id);
utils.setQueryData(["viewer.eventTypes"], {
...previousValue,
eventTypeGroups: [
...previousValue.eventTypeGroups.slice(0, groupIndex),
{ ...group, eventTypes: newList },
...previousValue.eventTypeGroups.slice(groupIndex + 1),
],
});
}
return { previousValue };
},
onError: (err, _, context) => {
if (context?.previousValue) {
utils.setQueryData(["viewer.eventTypes"], context.previousValue);
}
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
@@ -183,6 +223,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
showToast(err.message, "error");
}
},
onSettled: () => {
utils.invalidateQueries(["viewer.eventTypes"]);
},
});
const [isNativeShare, setNativeShare] = useState(true);
@@ -279,7 +322,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
/>
</Tooltip>
<Dropdown modal={false}>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
<DropdownMenuTrigger
asChild
data-testid={"event-type-options-" + type.id}
className="radix-state-open:rounded-r-md">
<Button
type="button"
size="icon"
@@ -439,7 +485,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</ul>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
variety="danger"
title={t("delete_event_type")}
confirmBtnText={t("confirm_delete_event_type")}
+1 -1
View File
@@ -18,7 +18,6 @@ import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
import { DOCS_URL } from "@calcom/lib/constants";
import { fetchUsername } from "@calcom/lib/fetchUsername";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
@@ -29,6 +28,7 @@ import Button from "@calcom/ui/Button";
import { Icon } from "@calcom/ui/Icon";
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
import { Form } from "@calcom/ui/form/fields";
import showToast from "@calcom/ui/v2/core/notifications";
import { getSession } from "@lib/auth";
import { inferSSRProps } from "@lib/types/inferSSRProps";
+12
View File
@@ -1,6 +1,7 @@
import React from "react";
import { Label, Input, TextField } from "@calcom/ui/form/fields";
import { Segment, SegmentOption } from "@calcom/ui/v2/core";
import { sandboxPage } from ".";
@@ -14,6 +15,17 @@ const page = sandboxPage(() => (
</div>
<div>
<TextField name="test-02" label="TextField" placeholder="it has an input baked in" />
{/* Adding to sandbox cause storybook doesnt like radix tailwind :S */}
<div className="pt-4">
<Segment label="Test" value="Test">
<SegmentOption value="Test">One</SegmentOption>
<SegmentOption value="2">Two</SegmentOption>
<SegmentOption value="3">Three</SegmentOption>
<SegmentOption value="4" disabled>
Four
</SegmentOption>
</Segment>
</div>
</div>
</div>
));
@@ -3,18 +3,17 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { Fragment } from "react";
import { WEBAPP_URL } from "@calcom/lib/constants";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui";
import { Alert } from "@calcom/ui/v2";
import { Alert, Button } from "@calcom/ui/v2";
import Badge from "@calcom/ui/v2/core/Badge";
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
import Meta from "@calcom/ui/v2/core/Meta";
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
import { SkeletonContainer, SkeletonText, SkeletonButton } from "@calcom/ui/v2/core/skeleton";
import { List, ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/modules/List";
import DestinationCalendarSelector from "@calcom/ui/v2/modules/event-types/DestinationCalendarSelector";
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
import { QueryCell } from "@lib/QueryCell";
@@ -36,6 +35,18 @@ const SkeletonLoader = () => {
);
};
const AddCalendarButton = () => {
const { t } = useLocale();
return (
<>
<Button color="secondary" StartIcon={Icon.FiPlus} href="/apps/categories/calendar">
{t("add_calendar")}
</Button>
</>
);
};
const CalendarsView = () => {
const { t } = useLocale();
const router = useRouter();
@@ -51,7 +62,11 @@ const CalendarsView = () => {
return (
<>
<Meta title="Calendars" description="Configure how your event types interact with your calendars" />
<Meta
title="Calendars"
description="Configure how your event types interact with your calendars"
CTA={<AddCalendarButton />}
/>
<QueryCell
query={query}
customLoader={<SkeletonLoader />}
@@ -63,7 +78,7 @@ const CalendarsView = () => {
<Icon.FiCalendar className="h-6 w-6" />
</div>
<div className="flex flex-col space-y-3">
<div className="flex w-full flex-col space-y-3">
<div>
<h4 className=" pb-2 text-base font-semibold leading-5 text-black">
{t("add_to_calendar")}
@@ -174,7 +189,7 @@ const CalendarsView = () => {
headline={t("no_calendar_installed")}
description={t("no_calendar_installed_description")}
buttonText={t("add_a_calendar")}
buttonOnClick={() => router.push(`${WEBAPP_URL}/apps/categories/calendar`)}
buttonOnClick={() => router.push("/apps/categories/calendar")}
/>
);
}}

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