Compare commits

...
282 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
zomars 9842c18d50 v2.1.0 2022-10-17 11:33:43 -06:00
Alex van Andel 68f05ce2c7 Uses utcOffset to display bookings according to current time & timeZone (#5050) 2022-10-17 17:28:57 +00:00
zomars 1aade4fc57 Hotfix: bookings/past browser crash 2022-10-17 11:10:23 -06:00
github-actions[bot]andCrowdin Bot 33d71f2390 New Crowdin translations by Github Action (#5043)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-17 17:50:30 +01:00
Peer RichelsenandHariom Balhara 44ef1f80e9 added QR code app, needs eventType.URL (#4701)
* wip added QR code app, needs settings

* added QR code settings, needs eventType.URL

* Make URL prop available to apps

* Add recurringEvent in available. It was missing earlier

* added autoAnimate to AppSettings

* Remove isSunrise demo prop

* Simplify schema even more

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2022-10-17 16:38:43 +00:00
9e3e1418c2 Fix disabled locations when rescheduling (#5046)
* fix phone number input when attendee phone number is default checked

* add selected location for rescheduling and remove default checked

* don't show locations when rescheduling on availability page

* fix type error

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2022-10-17 15:47:11 +02:00
Jeroen ReumkensandAlex van Andel 0284ffc75f Removed v2 pages directory, moved all pages to root and deleted old v1 pages and middleware redirect. (#5044)
* Removed v2 pages directory, moved all pages to root and deleted old v1 pages and middleware redirect.

* Removed v2 from imports path.

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-17 18:14:43 +05:30
Hariom Balhara aee1b3b596 Fix RHF data on bg update (#5045) 2022-10-17 11:21:49 +00:00
c1bc0286d1 Additional Notes Required (#4897)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-17 09:43:16 +00:00
48d1e95509 use v2 button (#5042)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-17 09:19:15 +00:00
Alex van Andel d8489a662e Adds a iso8601 helper function to parse a ISO Date String into a Date (#5039) 2022-10-17 09:07:49 +00:00
github-actions[bot]andCrowdin Bot 0fc1449dd9 New Crowdin translations by Github Action (#5037)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-17 09:53:41 +01:00
d5024d1b70 Fixed dropdown width according to items (#4995)
* Fixed dropdown width according to items

* Add the “Add” before the calendar type item

* Fix dropdown cut off on mobile

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2022-10-17 08:46:37 +00:00
Alex van Andelandkodiakhq[bot] b7161694a5 Update to message for invalid method (#5038)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-17 06:25:23 +00:00
Peer Richelsen cc7e4b3b33 fixed team URL in settings (#5031) 2022-10-17 11:40:06 +05:30
github-actions[bot]andCrowdin Bot 3b9653df0e New Crowdin translations by Github Action (#5028)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-16 22:30:33 +01:00
Leo Giovanetti 83442caf86 Fixing rescheduled email date (#5035) 2022-10-16 18:46:10 +01:00
Leo Giovanetti e6f912bf0c Fixing reschedule button in success page (#5033) 2022-10-15 19:38:39 -06:00
Carina Wollendorfer e75f200ff7 fix type (#5032) 2022-10-15 19:37:05 -06:00
Hariom Balhara 2d30f673e3 Fixes double bottom border in Integration List Item (#5026)
* Fixes double bottom border in Integration List Item

* Remove unused import
2022-10-15 11:08:01 -06:00
Leo GiovanettiandPeer Richelsen 2e83c7fbf8 Fixing bookings for all calendar-related apps (#5018)
* Fixing bookings for apps

* Missing assigning let var

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-15 11:02:24 -06:00
github-actions[bot]andCrowdin Bot 1b215e4faf New Crowdin translations by Github Action (#5022)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-15 11:15:17 +01:00
49429c87cb New Crowdin translations by Github Action (#5021)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-15 11:05:47 +01:00
cf423dda70 Add AppStore General Settings support (#4998)
* added weather in calendar wrapper

* added textfield to weather app, needs GeneralSettings abstraction

* add automation cateogry for n8n and zapier

* fix border radius for seperated apps

* create app specific settings in installed apps

* Fixed design issues caused by app settings

* add first version of template cards with logos

* Add link to zapier template

* Add missing translations

* fix mobile view

* better readable app description in mobile view

* code clean up

* fix issue that lisItem was always expanded

* code clean up

* code clean up

* fix import

* fix typo

* code clean up

* Add AppStore General Settings support

* add missing alt to img

* use FiShare2 icon instead of FiZap

* Remove duplicate entries

* Add border to AppSettings consistently

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-15 10:05:14 +00:00
6ee9de6dd8 Add links to Zapier templates in installed apps (#4982)
* add automation cateogry for n8n and zapier

* fix border radius for seperated apps

* create app specific settings in installed apps

* Fixed design issues caused by app settings

* add first version of template cards with logos

* Add link to zapier template

* Add missing translations

* fix mobile view

* better readable app description in mobile view

* code clean up

* fix issue that lisItem was always expanded

* code clean up

* code clean up

* fix import

* fix typo

* code clean up

* add missing alt to img

* use FiShare2 icon instead of FiZap

* Remove duplicate entries

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2022-10-15 08:05:37 +00:00
alannnc e54bdb4c46 Custom zoom response handler (#5017) 2022-10-14 18:24:57 -06:00
zomars 1455a6f603 Adds url schema to webhooks 2022-10-14 17:41:52 -06:00
zomars 9b980b3b85 Better handling of customInput error messages 2022-10-14 16:45:40 -06:00
github-actions[bot]andCrowdin Bot 6420218cb3 New Crowdin translations by Github Action (#5011)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-14 21:41:38 +01:00
Peer Richelsen f9ca763523 <Tips> in sidebar (#4999) 2022-10-14 19:38:05 +00:00
Joe Au-YeungandPeer Richelsen c4f3dec854 Fix time format on booking page (#5006)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-14 19:15:03 +00:00
zomars ff01da540d Avoids mixing metadata and tsx
Prevents circular dependencies
2022-10-14 12:51:28 -06:00
github-actions[bot]andCrowdin Bot 9021486cd7 New Crowdin translations by Github Action (#5007)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-14 19:41:28 +01:00
zomars 550f98a636 Splits appData schemas to prevent circular dependencies 2022-10-14 12:35:44 -06:00
Peer Richelsen 9a4746844f fixed booking page padding (#5009) 2022-10-14 18:04:54 +00:00
Hariom BalharaandPeer Richelsen 271d4319b9 Introduce EventTypeAppCard in app-store and make it super easy to add it through CLI - Also adds Fathom app (#4727)
* Add OmniInstall button

* Make AppCards configurable by the app itself

* Make OmniInstallAppButton not redirect

* Fixes

* Add extendsFeature support to CLI

* Move to automatic file generation approach as dynamic import checking doesnt work correctly

* Use zod everywhere consistenly for metadata and fix all TS issues

* Fix viewer.eventTypes endpoint. Make prisma base select and _ prefixed models consistent in expecting scalars only

* Remove unnecessary zod parsing of event-types as it is making the scope of the PR huge

* Fix UI TS errors

* wip

* Add zod types support in EventTypeAppCard.tsx

* Fixes during PR review and other failing tests

* Remove unused app

* Fix stripe installation flow

* More fixes

* Fix apps and active apps count

* self review

* Add loading attribute to OmniInsall button

* Handle empty state

* Improve types

* Fix stripe app installation bug

* added fathom app (#4804)

* added fathom app wrapper, needs script injection to public booking page

* new logo

* Add Fathom script support on booking pages and add it as an eventTypeapp

* Add automation and analytics apps

* Add missing pieces for analytics category

* Rename BookingPageScripts to BookingPageTagManager

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

* Fix lint error

* Fix runtime error with legayAppData being undefined

* Remove duplicate automation key

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-14 10:24:43 -06:00
Peer Richelsen 215e3c93d6 remove unused workflow (#5000) 2022-10-14 09:33:08 -03:00
Nafees NazikandAlex van Andel 670aed9fe0 fix: add a unique key (#4711)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-14 12:33:32 +01:00
Hariom BalharaandAlex van Andel 8583d9b2e6 Embed Improvements: Add auto-scroll support back, reduce CLS, remove loader for Routing Form embed (#4819)
* Add back auto-scroll fix with proper check of visibilith

* Do improvement in CLS and dont show "Loading" text

* Improve experience with routing forms embed

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-14 10:10:54 +00:00
Carina WollendorferandCarinaWolli 88cad2e15f Workflow security fixes (#4996)
* security fix to not add even-types from other users

* throw unauthroized error if a step does not belong to workflow

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-10-14 10:52:20 +01:00
1c165bd934 Fix/fallback to calvideo (#4992)
* try/catch on createMeeting zoom

* add handle response body from zoom

* db invalid credential flag

* Update VideoApiAdapter.ts

* Added notes and try/catches

* Remove double response error validation

* Fallback to calvideo

* Fix types for credentials

* Add nullable to invalid field from credential table

* Fix calendar service credential creation with bad type

* revert db migration invalid on credential table

* Revert "Fix types for credentials"

This reverts commit 1623305aea23a33c39ba7b6fb5604dd71c6205d7.

* Revert "Fix calendar service credential creation with bad type"

This reverts commit 3fd3b08f2ee6c7019428a8ff454d371e23759ad7.

* Remove migration

* Fix for property name for api key on dailyvideo

* remove unused prop from credential

* Revert "Fix for property name for api key on dailyvideo"

This reverts commit 32976934e05e6923210718ccd5634c1357828f65.

* Tests fixes

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2022-10-14 02:36:43 +00:00
zomars 74a16e28f4 e2e now needs daily as fallback 2022-10-13 20:26:04 -06:00
github-actions[bot]andCrowdin Bot 3e417330b5 New Crowdin translations by Github Action (#4988)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-13 17:50:59 +01:00
Joe Au-YeungandPeer Richelsen 3af31860bb Close menu and prevent overscroll (#4739)
* Close menu and prevent overscroll

* Update Shell.tsx

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-13 09:50:08 -06:00
Carina WollendorferandCarinaWolli f0aded92db Add skeleton loader to offer seats (#4987)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-10-13 13:33:41 +01:00
Alex van Andel a28325e69a Adjust booking list startTime to the timeZone of the logged in user (#4984) 2022-10-13 12:01:11 +01:00
github-actions[bot]andCrowdin Bot f949cf1bd1 New Crowdin translations by Github Action (#4977)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-13 10:36:15 +01:00
zomars ba31c9dcb5 GitHub Workflow: when label: “help wanted” add to Project 2022-10-12 10:33:39 -06:00
Omar LópezandAlex van Andel e5af99f572 Moved booking logic to calcom/features (#4960)
* Moved booking logic to calcom/features

Needed to reuse on public API

* Type fixes

* Cleanup

* Cleanup

* Cleanup

* Restored still needed V1 pages

This reverts commit 790a304a6aa3b9e16525ea3b015bef773f36ded1.

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-12 13:04:51 +00:00
Peer Richelsen c40ff2e6bb added new license required /signup page (#4966) 2022-10-12 13:52:59 +01:00
Carina Wollendorfer b1656c7900 don't count unconfirmed recurring bookings that are in the past (#4968) 2022-10-12 11:08:30 +01:00
a9b18496e0 refactor: left booking information box component (#4251)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
2022-10-12 08:39:14 +00:00
github-actions[bot]andCrowdin Bot d35ad510c9 New Crowdin translations by Github Action (#4955)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-12 09:28:17 +01:00
+9 6e834aa8eb Delete Dialog once closed, makes the entire app unresponsive. (#4906)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: RubensRafael <rubensrafael2@live.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: MuriloAmarals <muralha2000@gmail.com>
Co-authored-by: Murilo Amaral <87545137+MuriloAmarals@users.noreply.github.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: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: C000Ldude <coolmagnas@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Eman <emmanuelgatwech@gmail.com>
Co-authored-by: Olusanya Timothy <48022904+seunexplicit@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-12 12:55:37 +05:30
5293db29d9 Feat Booking Limits (#4759)
* Add db relevant stuff

* Basic UI there still buggy

* This UI is hard - some progress

* Fix awful state mangament

* Fix re-ordering

* Working UI logic!

* Partical working minMax function

* Fix min max

* bookingLImits api + tests

* Moved checkBookingLimits to backend only code

* Fix httperror import

* Return busy times

* Remove avaliablity calc

* Working for everything but year

* Remove redundant + fix async forloop

* Add compatible type

* Future proof with evenTypeId filter

* Fix commonjson

* Sorting + validation + tests + passing

* Add empty test

* Move validation check  to backend

* Add bookinglimits in trpc

* Add test for undefined

* Apply suggestions from code review

Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>

* Update apps/web/components/v2/eventtype/EventLimitsTab.tsx

Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>

* Rename value for eligiability

* Rename keyof type

* status code

* Fix toggle not toggling off

* Update apps/web/pages/v2/event-types/[type]/index.tsx

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

* Update apps/web/pages/v2/event-types/[type]/index.tsx

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

* Change back to undefined as it is working for sean. See if it fails on testapp

* Fixing test builder

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
2022-10-12 05:29:04 +00:00
Alex van Andel e772b4203e Unlicensed should return false, not error 2022-10-12 02:40:52 +01:00
Alex van Andel 6cc8fcae52 Handle double quotes situation in checkLicense function (#4961) 2022-10-11 19:34:27 -06:00
Hariom Balhara 7e5c686e26 Switch to using tRPC as router.push weirdly doesnt refetch when using middleware and that too after build (#4954) 2022-10-11 15:52:18 +00:00
2c6c768145 New Crowdin translations by Github Action (#4949)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-11 16:32:52 +01:00
Peer Richelsen a5fee47989 fixed n8n link (#4953) 2022-10-11 16:29:52 +01:00
github-actions[bot]andCrowdin Bot bceeee70e0 New Crowdin translations by Github Action (#4940)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-11 15:17:42 +01:00
zomars baecd923af Makes title and uid non-empty for booking references 2022-10-10 21:33:50 -06:00
zomars 1fe1130af1 If a team is deleted, it cascades to memberships 2022-10-10 19:24:46 -06:00
zomars 49805054ba Updates deprecated nonempty 2022-10-10 18:28:48 -06:00
zomars e2362d8f2a Makes team names and slug required 2022-10-10 18:28:48 -06:00
Peer Richelsen 9453c1fc3a return null if impersonating (#4939) 2022-10-10 17:39:15 -06:00
47e496fded Remove <Loader/> from app setup pages and team user availability view (#4931)
* remove loader from setup screens + zapier loading

* remove loader from team user availability

* create skeleton loader

* code clean up

* code clean up

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-10 17:31:16 -06:00
zomars 0289e727ad sync monorepo packages 2022-10-10 17:22:52 -06:00
zomars 04b5125121 Sync submodules 2022-10-10 17:22:36 -06:00
zomars 368d1afe27 v2.0.5 2022-10-10 17:01:53 -06:00
zomars 2069fd7248 Update settings.json 2022-10-10 16:27:31 -06:00
7ed21b692e #4306: Added darkmode support for select by styling via tailwind (#4824)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-10 18:50:43 +00:00
4fcdc7bf5c added badge for unconfirmed bookings (#4912)
* added badge for unconfirmed bookings

* added link to unconfirmed bookings

* added rounded prop to badge, added unconfirmed badge to botto nav

* Supporting recurring events + bug fix

* Tooltip and hover style

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
2022-10-10 17:00:09 +00:00
2a0a293f8c Add new workflow action: Send email to specific email address (#4929)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-10 13:40:20 +00:00
1b15b71a24 Feat/improve animation on public booking page (#4848)
* Improve responsive layout for timezone select on booking page.

* Close TZ dropdown on booking page when it's hidden for mobile/desktop, this prevents having a dropdown menu floating around.

* Improved animation for showing timeslots on booking page.

* Changed back button on booking page to sm+ to md+ like it was before.

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-10 13:24:06 +00:00
github-actions[bot]andCrowdin Bot 5d9889a90c New Crowdin translations by Github Action (#4927)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-10 13:42:19 +01:00
Peer Richelsen a7ee3366eb Update CONTRIBUTING.md 2022-10-10 09:31:33 +01:00
c68e7595fe New Crowdin translations by Github Action (#4923)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-10 09:28:18 +01:00
Herbert Mühlburger 3f6be3b2c0 correct german spelling (#4925) 2022-10-10 09:27:44 +01:00
Hariom Balhara f1647248a2 PaymentPage: Fix localStorage access issue in incognito (#4922)
* localStorage isnt available at server and in incognito mode

* Do translation server side for Payment Page
2022-10-10 08:11:47 +00:00
Leo Giovanettiandkodiakhq[bot] d7db1ad303 Hiding event type loses recurring config (#4911)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-09 12:36:39 +00:00
Udit Takkar bd48fd01b1 fix: remove hr add spacing, fix link in env file (#4916) 2022-10-09 11:08:47 +00:00
github-actions[bot]andCrowdin Bot fabf692a08 New Crowdin translations by Github Action (#4904)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-08 18:10:55 +01:00
Alex van Andel ab62fc5d47 Bugfix/dst available hour mismatch (#4909)
* Need to use local time, not inviteeDate

* Use selected timeZone to perform localWorkingHours logic

* Adding comment

* More comments
2022-10-07 16:10:48 -06:00
zomars 0c4cb2e2b0 Added denullish utils for zod
Needed for API refactoring
2022-10-07 14:48:41 -06:00
Carina WollendorferandCarinaWolli 7d77132102 Add 'after event ends' trigger to workflows (#4882)
* add after event ends workflow trigger to enum

* add after meetings ends trigger

* fix label for time span

* add deleted label for message template

* add lost changes from merge conflict

* set reminder for already existing bookings

* fix label 'How long before event starts/ends'

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-10-07 12:18:28 -06:00
Joe Au-Yeung 50ac5f2892 Fix MS Teams missing refresh token (#4910) 2022-10-07 13:59:58 -04:00
Peer Richelsen fd12fcc4b8 v2 for help dialog (#4901) 2022-10-07 13:21:49 +01:00
+2 58453d0f2f /event-types/detail wrong empty screen for workflows (#4757)
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: Matheus Muniz <matheusmuniz100@hotmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: Olusanya Timothy <48022904+seunexplicit@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 Muniz <matheusmuniz100@hotmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: Olusanya Timothy <48022904+seunexplicit@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2022-10-07 09:25:30 +00:00
Hariom Balhara 44fd1b9fef Improve default app README.mdex (#4893) 2022-10-07 07:29:09 +00:00
Leo Giovanetti 0098591a56 Applying feedback from a merged PR (#4887) 2022-10-06 17:38:50 -03:00
Leo Giovanetti 41c1e960f8 Adjusting jest and expanding test builders (#4837)
* Adjusting jest and expanding test builders

* Removing unneeded jest config
2022-10-06 13:29:20 -06:00
Leo Giovanetti 4e49d32c61 Fixing unconfirmed/recurring bookings (#4881) 2022-10-06 13:23:22 -06:00
+9 0b29c5063a Wrong css in event configuration page buttons (#4869)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.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: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: C000Ldude <coolmagnas@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Eman <emmanuelgatwech@gmail.com>
Co-authored-by: Olusanya Timothy <48022904+seunexplicit@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Omar López <zomars@me.com>
2022-10-06 12:16:32 -06:00
zomars 95a92567cb Update change-username.e2e.ts 2022-10-06 12:15:14 -06:00
+7 9e183e6ad1 Remove black ring on focus when copying private link (#4883)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: MuriloAmarals <muralha2000@gmail.com>
Co-authored-by: Murilo Amaral <87545137+MuriloAmarals@users.noreply.github.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: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: C000Ldude <coolmagnas@gmail.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Eman <emmanuelgatwech@gmail.com>
Co-authored-by: Olusanya Timothy <48022904+seunexplicit@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-06 15:18:16 +00:00
Peer Richelsen 7900f8bab5 fixes to booking page (#4876) 2022-10-06 14:33:57 +00:00
Hariom Balharaandkodiakhq[bot] 50904d2588 useForm defaultValues should be available initially (#4863)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-06 13:37:52 +00:00
github-actions[bot]andCrowdin Bot 6f811f4f0a New Crowdin translations by Github Action (#4877)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-06 14:14:10 +01:00
sean-brydonandBailey Pumfleet d9000d0cbe Fix height on trash (#4875)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-10-06 13:33:51 +01:00
sean-brydonandBailey Pumfleet ff78e66051 Add successful booking redirect URL to booking page (#4871)
* Add redirect URL field and switch

* Remove and add new translation for description

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-10-06 13:32:11 +01:00
Hariom Balharaandkodiakhq[bot] 7c30f8ca4b Routing Forms Type nigration (#4861)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-06 12:21:04 +00:00
Jeroen Reumkens 0ebce77a7b #4851: Always take timeformat on booking page from profile if user is logged in. Also hides timeformat toggle on booking page for logged in users. For non loggedin users we still infer by looking at browser timezone, after that we set a localstorage which we will look at, and which will get updated when the user updates the toggle in the timezone dropdown. (#4865) 2022-10-06 10:25:41 +00:00
sean-brydon 4282adac4b Fix text wrap (#4867) 2022-10-06 10:57:57 +01:00
Nafees Nazikandkodiakhq[bot] ef611e2360 fix: fixes multiple issues in routing form (#4676)
* feat: update forms procedure to match getServerSideProps

* feat: init react query and use prefetched data

* feat: invalidate onSettled while creating a form

* feat: use optimistic updates when deleting

* fix: add optional chaining

* feat: add server-side translation

* feat: add correct translation key

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-06 06:49:03 +00:00
github-actions[bot]andCrowdin Bot 10ab86b332 New Crowdin translations by Github Action (#4772)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-10-05 20:23:28 +01:00
Peer Richelsen f04cfef0af reduced padding top on mobile onboarding (#4856) 2022-10-05 18:46:20 +00:00
Alex van Andel 50151403c4 v2.0.4 2022-10-05 16:27:01 +01:00
Peer RichelsenandAlex van Andel 7aa131f349 added TimezoneChangeDialog (#4818)
* nit

* added timezoneChangeDialog

* prevent interactOutside

* nit

* fixed title prop clash

* nit

* remind in one day if clicked outside

* Use utcOffset instead of tz string for checking diff

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-05 14:33:29 +00:00
Jeroen Reumkens d88bdc368b Ensure we have to correct tabindexes for addons. Before addons AFTER the input still would get tab focus before the input. (#4849) 2022-10-05 14:22:06 +00:00
Alex van Andel 255938fda0 Remove 'avatar' from query (#4846) 2022-10-05 13:37:42 +00:00
Jeroen ReumkensandPeer Richelsen 5766d6f5f4 Improve responsive layout of timezone select on booking page (#4839)
* Improve responsive layout for timezone select on booking page.

* Close TZ dropdown on booking page when it's hidden for mobile/desktop, this prevents having a dropdown menu floating around.

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-10-05 13:11:26 +00:00
Peer Richelsen 9d299c6d5f fixed event type z-index (#4844) 2022-10-05 12:58:59 +00:00
f49783b453 Bugfix/reinstate tab name with fixed test (#4842)
* added ?tabName=setup to event-type detail link to give it active status (#4830)

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* Use pathname instead of href directly

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-05 11:55:11 +00:00
Jeroen Reumkens 246fce3ab8 #3785: Converted billing settings page to v2. (#4795) 2022-10-04 22:54:15 +00:00
zomars 82f262f60d Quickfix for settings layout on mobile 2022-10-04 15:24:59 -05:00
zomars da8d2da368 v2.0.3 2022-10-04 14:46:51 -05:00
alannncandOmar López f01058e073 fix/error-while-confirm-booking (#4834)
* Add try/catch on workflows while confirming a booking

* Revert "added ?tabName=setup to event-type detail link to give it active status (#4830)"

This reverts commit 11c44db609.

Co-authored-by: Omar López <zomars@me.com>
2022-10-04 14:46:17 -05:00
zomars 8e369d3fb5 Submodule sync 2022-10-04 13:10:16 -05:00
Peer Richelsenandkodiakhq[bot] 11c44db609 added ?tabName=setup to event-type detail link to give it active status (#4830)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-04 17:03:31 +01:00
Peer RichelsenandAlex van Andel 9d911cbb59 fixed dropdown in safari (#4828)
* fixed dropdown in safari

* nit

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-04 15:50:15 +00:00
Alex van Andel 30a887df6f Some tweaks to the ui - use button - use flex justify-between (#4826) 2022-10-04 15:37:20 +00:00
62fea25eb9 fix/connected calendar list different states (#4821)
* Display error if trpc query failed, catch error on getting calendar and cleanup to don't leak credentials to client

* Add calendar connection broke error

* Edit style on alert and disconnecting button

* Ts errors and added translation

* Type fix

* Key and text change for broken calendar connection

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-04 14:56:38 +00:00
Alex van Andel d43d8700d0 Fixes global console error (#4825) 2022-10-04 14:01:32 +00:00
Peer Richelsen e7ed20ea27 fixed border on dark mode success (#4823) 2022-10-04 13:43:49 +01:00
Peer RichelsenandBailey Pumfleet 5e12b5defe new button in logout (#4816)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-10-04 09:31:13 +00:00
Joe Au-YeungandBailey Pumfleet d241a522fb Add placeholder (#4803)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-10-04 10:20:21 +01:00
+2 ae929f27b0 /workflows /detail-view - unusual scroll behaviour (#4806)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: C000Ldude <coolmagnas@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: Olusanya Timothy <48022904+seunexplicit@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-10-04 09:17:30 +00:00
Syed Ali Shahbaz 369d7c4e28 adds custom filter allowing eml format (#4815) 2022-10-04 10:02:28 +01:00
Jeroen Reumkens 4438ca2a63 Improve password peek positioning accross app and website. (#4810) 2022-10-03 19:52:57 +00:00
Hariom Balhara 75589db569 Hotfix: Fix Broken webhooks (#4807) 2022-10-04 00:33:47 +05:30
Alex van Andel 3752ac767a Remove eye for todays release (#4802) 2022-10-03 17:08:17 +01:00
+3 f3e2cf5075 Onboarding, input validation error isn't positioned correctly (#4758)
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: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Eman <emmanuelgatwech@gmail.com>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: Olusanya Timothy <48022904+seunexplicit@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-01 21:01:41 +00:00
Peer Richelsen 92a29593aa Update README.md 2022-10-01 21:47:34 +01:00
Peer Richelsen 367c5d89cc removed capitalize from headlines (#4790) 2022-10-01 11:50:27 +01:00
f21f36547c /event-types /team-detail /scheduling type - double divider under team members (#4485)
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: hustle-ps <grace.devolop@gmail.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Olusanya Timothy <48022904+seunexplicit@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: hustle-ps <grace.devolop@gmail.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Olusanya Timothy <48022904+seunexplicit@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-10-01 02:59:20 +00:00
Joe Au-Yeung d3f8d954e7 Update Close.com info (#4777) 2022-09-30 20:22:47 +01:00
27372d624d add skeleton loader for workflows in event type settings (#4774)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-09-30 19:39:23 +01:00
a5d2f2444a Fixes 'How to use Additional Inputs as Variables' dialog in workflows (#4773)
* add i18n translations and fix dialog width and height

* fix typo

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-09-30 14:41:12 +00:00
6d45e1c7c7 #4533: Show correct time format on booking page (#4771)
* #4533: Store timeformat preferences in localstorage when changing profile. This way you see the correct format on the booking page. Also abstracted the reuse of the localstorage key for timeformat into a small util function, to prevent typos in the future.

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

* 4533: Changed 24hfromlocalstorage values from strings to real booleans for better DX.

* Update apps/web/lib/clock.ts

Co-authored-by: alannnc <alannnc@gmail.com>

* Make code shorter by only handling remaining cases

Co-authored-by: alannnc <alannnc@gmail.com>

* Remove unnecessary boolean check

Co-authored-by: alannnc <alannnc@gmail.com>

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-09-30 14:45:28 +02:00
48419469a6 #4445: Fix to prevent bookings with long titles or a lot of content from overflowing on the page. (#4769)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2022-09-30 11:47:20 +01:00
Joe Au-Yeung 045ebb6e01 Fix RadixUI dropdown 1.0.0 (#4767)
* Add portal part

* Add portal part to pages

* Add portal part to pages
2022-09-30 12:29:22 +02:00
Jeroen Reumkens c78ea5de0b Added 100vh fix for ios to make sure it always fills the screen height. By default 100vh on ios sometimes will and sometimes won't take into account the address bar height, causing on eg the event types or bookings pages the content to be behind our bottom nav. (#4770) 2022-09-30 10:12:22 +01:00
github-actions[bot]andCrowdin Bot d1a787315c New Crowdin translations by Github Action (#4762)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-09-29 19:23:43 +01:00
ce1f1f3c47 Upgrade to Radix 1.0 (#4746)
* Change package.json version and yarn lock

* Add popover

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-09-29 18:22:07 +01:00
2f8bd3c07a add trpc v10 (#4683)
* revert me later

* let's see if this builds

* fix dupe proc

* fix: v10 works

* fix type error

* fix type error

* fix type errors

* fix more

* add example procedure

* spreading not needed

* Update yarn.lock

* Revert "revert me later"

This reverts commit 0c8c15d0577e0c287223039c7b6958b2b0c12c69.

Co-authored-by: Chris Bautista <chrisbautista@netflix.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-09-29 16:58:29 +00:00
Omar LópezandAlex van Andel d2f27a0463 Fixes for advanced tabs form logic (#4731)
* Fixes for advanced tabs form logic

* Typefix

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-09-29 16:46:47 +00:00
Jeroen Reumkensandkodiakhq[bot] 9134ad8c5e Improved team layout when blocks have unequal content length. (#4763)
* Improved team layout when blocks have unequal content length.

* Removed unused translation in members component.

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-09-29 16:34:01 +00:00
Alex van Andel 506036e751 Fixes few i18n related bugs on change password (#4755)
* Fixes few i18n related bugs

* Use PasswordField
2022-09-29 16:19:03 +00:00
5e80f5056d Ensure currentUsername is set before comparing it with username (#4750)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-09-29 17:05:01 +01:00
9304ca1415 Fix long url and show password (#4738)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-09-29 11:40:37 -03:00
github-actions[bot]andCrowdin Bot fb246fdee6 New Crowdin translations by Github Action (#4754)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-09-29 14:31:30 +01:00
7b9e77f43a Fixing border in installed apps (#4745)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-09-29 13:26:36 +00:00
47df21322d Fixes translation issue of dynamic variables in workflows (#4734)
* replace all underscores for variable translations

* use g instead of replaceAll

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2022-09-29 15:07:22 +02:00
6017806763 Fix connected calendar list (#4743)
* Fix connected calendar list

* Fixed invalid params in setDestinationCalendar

* Attempt to fix tests by changing viewport size, bug on /booking pages

* Fixes BookingListItem + another user.id param removed

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-09-29 12:58:53 +00:00
Alex van Andel b347f29d6c Feature/deprecate title translations (#4682)
* wip

* Aims to deprecate _title keys

* Removed _title translation

* Revert "wip"

This reverts commit 3e33fc6b1ebc064ddfacd8ebc2b63b2ba009d416.
2022-09-29 12:41:40 +00:00
zomars c26cf74aec Revert "Increase timeouts (#4716)"
This reverts commit 145fb09b73.
2022-09-29 14:23:52 +02:00
zomars 760ed74a11 Fixes broken E2E tests
Introduced in #4657
2022-09-29 14:19:04 +02:00
Hariom BalharaandAlex van Andel 145fb09b73 Increase timeouts (#4716)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-09-29 10:06:57 +00:00
Peer Richelsen dd14e827e1 added cancel button to team creation (#4756)
* added cancel button

* nit

* nit
2022-09-29 10:47:51 +01:00
246c90c10f public page: border and padding (#4730)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Klinger Matheus <50892465+KlingerMatheus@users.noreply.github.com>
Co-authored-by: Klinger Matheus <klinger.matheus@gitstart.dev>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Olusanya Timothy <48022904+seunexplicit@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-09-29 10:24:28 +01:00
Alexey Pavlov a42f703c1d fix: calendar spacings for mobile on availability page (#4728) 2022-09-29 10:21:52 +01:00
Leo Giovanetti 7256dbf84f Fix updating slug (#4747) 2022-09-28 23:07:55 +01:00
github-actions[bot]andCrowdin Bot f5faebad67 New Crowdin translations by Github Action (#4744)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-09-28 22:19:00 +01:00
+3 be15affa5d fix padding (#4673)
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 Muniz <matheusmuniz100@hotmail.com>
Co-authored-by: Grace Nshokano <grace.devolop@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Olusanya Timothy <48022904+seunexplicit@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-09-28 22:02:07 +01:00
58dd5308ac Improved load state (#4696)
* Improved load state

* Don't use editable heading if it is readonly

* Fix ts errors

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-09-28 19:05:28 +01:00
14a6c5a03f feat: add transition in toasts (#4658)
* feat: add transition in toasts

* feat: toast animation improved

* fix: type check in toast

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-09-28 19:01:19 +01:00
5af20f5a7b New Crowdin translations by Github Action (#4742)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-09-28 18:57:58 +01:00
Peer Richelsen 8478363a23 minor layout fixes (#4741)
* fixed back button, fixed app store detail on mobile

* fixed settings padding and go back button

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

* Update packages/ui/v2/core/navigation/tabs/VerticalTabItem.tsx
2022-09-28 18:56:59 +01:00
19b3157c94 New Crowdin translations by Github Action (#4729)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-09-28 18:51:19 +01:00
Peer Richelsen d4dd451c26 Update packages/ui/v2/core/Shell.tsx 2022-09-28 18:49:23 +01:00
Peer Richelsen 6c046f6507 fixed back button, fixed app store detail on mobile (#4737) 2022-09-28 18:46:14 +01:00
Peer Richelsen a792d31939 Update package.json 2022-09-28 17:13:04 +01:00
654 changed files with 17087 additions and 17545 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ LARK_OPEN_VERIFICATION_TOKEN=""
# - WEB3
# Used for the Web3 plugin
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/web3/README.md
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/rainbow/README.md
ALCHEMY_API_KEY=""
INFURA_API_KEY=""
# *********************************************************************************************************
-16
View File
@@ -1,16 +0,0 @@
name: Add PRs to project
on:
pull_request:
types:
- opened
jobs:
add-PR-to-project:
name: Add PR to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.1.0
with:
project-url: https://github.com/orgs/calcom/projects/1
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
+2 -2
View File
@@ -17,6 +17,6 @@ jobs:
steps:
- uses: actions/add-to-project@v0.1.0
with:
project-url: https://github.com/orgs/calcom/projects/1
project-url: https://github.com/orgs/calcom/projects/9
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
labeled: 🐛 bug, 🎨 needs design, 🙋🏻‍♂️help wanted, 🚨 urgent
labeled: 🙋🏻‍♂️help wanted
@@ -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.
+2 -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:
@@ -25,6 +25,7 @@ jobs:
GOOGLE_LOGIN_ENABLED: true
# CRON_API_KEY: xxx
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
+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:
+1 -1
View File
@@ -6,5 +6,5 @@
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"spellright.language": ["en"],
"spellright.documentTypes": ["markdown"]
"spellright.documentTypes": ["markdown", "typescript"]
}
-17
View File
@@ -4,23 +4,6 @@ Contributions are what make the open source community such an amazing place to l
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
## Areas of expertise
### Legend
✅ = has knowledge
🥇 = is their main priority
⚠️ = is the only one with knowledge
👀 = has no knowledge but wants to be onboarded
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://dynamic-svgs.vercel.app/image.svg?dark">
<img alt="Areas of expertise table" src="https://dynamic-svgs.vercel.app/image.svg">
</picture>
## Developing
The development branch is `main`. This is the branch that all pull
+3 -3
View File
@@ -31,13 +31,13 @@
<a href="https://news.ycombinator.com/item?id=26817795"><img src="https://img.shields.io/badge/Hacker%20News-311-%23FF6600" alt="Hacker News"></a>
<a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
<a href="https://github.com/calcom/cal.com/pulse"><img src="https://img.shields.io/github/commit-activity/m/calcom/cal.com" alt="Commits-per-month"></a>
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-%2412%2Fmonth-brightgreen" alt="Pricing"></a>
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-Free-brightgreen" alt="Pricing"></a>
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
<img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" />
<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.
+9 -8
View File
@@ -15,14 +15,15 @@
"dependencies": {
"@calcom/dayjs": "*",
"@calcom/ui": "*",
"@radix-ui/react-avatar": "^0.1.4",
"@radix-ui/react-collapsible": "^0.1.0",
"@radix-ui/react-dialog": "^0.1.0",
"@radix-ui/react-dropdown-menu": "^0.1.1",
"@radix-ui/react-id": "^0.1.0",
"@radix-ui/react-radio-group": "^0.1.1",
"@radix-ui/react-slider": "^0.1.1",
"@radix-ui/react-switch": "^0.1.1",
"@radix-ui/react-avatar": "^1.0.0",
"@radix-ui/react-collapsible": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-dropdown-menu": "^1.0.0",
"@radix-ui/react-id": "^1.0.0",
"@radix-ui/react-popover": "^1.0.0",
"@radix-ui/react-radio-group": "^1.0.0",
"@radix-ui/react-slider": "^1.0.0",
"@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
"next": "^12.2.5",
"next-transpile-modules": "^9.0.0",
@@ -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";
+5 -5
View File
@@ -11,14 +11,14 @@ export type GateState = {
type GateProps = {
children: React.ReactNode;
gates: Gate[];
metadata: JSONObject;
appData: JSONObject;
dispatch: Dispatch<Partial<GateState>>;
};
const RainbowGate = dynamic(() => import("@calcom/app-store/rainbow/components/RainbowKit"));
// To add a new Gate just add the gate logic to the switch statement
const Gates: React.FC<GateProps> = ({ children, gates, metadata, dispatch }) => {
const Gates: React.FC<GateProps> = ({ children, gates, appData, dispatch }) => {
const [rainbowToken, setRainbowToken] = useState<string>();
useEffect(() => {
@@ -31,12 +31,12 @@ const Gates: React.FC<GateProps> = ({ children, gates, metadata, dispatch }) =>
for (const gate of gates) {
switch (gate) {
case "rainbow":
if (metadata.blockchainId && metadata.smartContractAddress && !rainbowToken) {
if (appData.blockchainId && appData.smartContractAddress && !rainbowToken) {
gateWrappers = (
<RainbowGate
setToken={setRainbowToken}
chainId={metadata.blockchainId as number}
tokenAddress={metadata.smartContractAddress as string}>
chainId={appData.blockchainId as number}
tokenAddress={appData.smartContractAddress as string}>
{gateWrappers}
</RainbowGate>
);
+3 -1
View File
@@ -10,7 +10,9 @@ export function useViewerI18n() {
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
* We intend to not cache i18n query
**/
context: { skipBatch: true },
trpc: {
context: { skipBatch: true },
},
});
}
@@ -1,5 +1,4 @@
import React from "react";
import Select from "react-select";
import { OptionProps } from "react-select";
import { InstallAppButton } from "@calcom/app-store/components";
@@ -8,6 +7,7 @@ import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App";
import { Button } from "@calcom/ui";
import { Icon } from "@calcom/ui/Icon";
import { Select } from "@calcom/ui/v2";
import { QueryCell } from "@lib/QueryCell";
@@ -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>{data.label}</p>
<p className="text-left">{`${t("add")} ${data.label}`}</p>
</Button>
);
}}
@@ -35,7 +35,7 @@ const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App
) : (
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
<Icon.FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
<p>{t("add_new_calendar")}...</p>
<p>{t("install_new_calendar_app")}</p>
</Button>
);
};
@@ -63,27 +63,45 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
return (
<Select
name="additionalCalendar"
placeholder={t("install_another")}
placeholder={
<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}
styles={{
placeholder: (defaultStyles) => {
return {
...defaultStyles,
color: "#3E3E3E",
marginLeft: "3px",
};
},
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "6px",
};
},
}}
isSearchable={false}
className="min-w-52 block w-full flex-1 rounded-none rounded-r-sm border-gray-300 text-sm font-medium text-gray-700"
isLoading={isLoading}
components={{ Option: ImageOption }}
styles={{
menu: (defaultStyles) => ({
...defaultStyles,
"@media only screen and (max-width: 640px)": {
left: "0",
},
width: "max-content",
right: "0",
}),
control: (defaultStyles) => ({
...defaultStyles,
padding: "0",
border: "0",
borderRadius: "inherit",
}),
dropdownIndicator: (defaultStyles) => ({
...defaultStyles,
display: "none",
}),
valueContainer: (defaultStyles) => ({
...defaultStyles,
padding: "0",
}),
placeholder: (defaultStyles) => ({
...defaultStyles,
margin: "0",
}),
}}
/>
);
}}
-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 min-w-[672px] 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>
);
}
@@ -1,26 +1,25 @@
import { useMutation } from "@tanstack/react-query";
import Link from "next/link";
import { Fragment } from "react";
import { useMutation } from "react-query";
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 Button from "@calcom/ui/Button";
import { Icon } from "@calcom/ui/Icon";
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
import { Alert, EmptyScreen } from "@calcom/ui/v2";
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>;
@@ -141,6 +140,7 @@ function CalendarList(props: Props) {
);
}
// todo: @hariom extract this into packages/apps-store as "GeneralAppSettings"
function ConnectedCalendarsList(props: Props) {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.connectedCalendars"], { suspense: true });
@@ -163,6 +163,7 @@ function ConnectedCalendarsList(props: Props) {
title={item.integration.title}
logo={item.integration.logo}
description={item.primary?.externalId || "No external Id"}
separate={true}
actions={
<div className="flex w-32 justify-end">
<DisconnectIntegration
@@ -173,23 +174,27 @@ function ConnectedCalendarsList(props: Props) {
/>
</div>
}>
{!fromOnboarding && (
<>
<p className="px-4 pt-4 text-sm text-neutral-500">{t("toggle_calendars_conflict")}</p>
<ul className="space-y-2 p-4">
{item.calendars.map((cal) => (
<CalendarSwitch
key={cal.externalId}
externalId={cal.externalId}
title={cal.name || "Nameless calendar"}
type={item.integration.type}
defaultSelected={cal.isSelected}
destination={cal.externalId === props.destinationCalendarId}
/>
))}
</ul>
</>
)}
<div className="border-t border-gray-200">
{!fromOnboarding && (
<>
<p className="px-4 pt-4 text-sm text-neutral-500">
{t("toggle_calendars_conflict")}
</p>
<ul className="space-y-2 p-4">
{item.calendars.map((cal) => (
<CalendarSwitch
key={cal.externalId}
externalId={cal.externalId}
title={cal.name || "Nameless calendar"}
type={item.integration.type}
defaultSelected={cal.isSelected}
destination={cal.externalId === props.destinationCalendarId}
/>
))}
</ul>
</>
)}
</div>
</IntegrationListItem>
) : (
<Alert
@@ -258,11 +263,11 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
<ShellSubHeading
title={t("calendar")}
subtitle={t("installed_app_calendar_description")}
className="mb-0 flex flex-wrap items-center gap-4 md:mb-3 md:gap-0"
className="mb-0 flex flex-wrap items-center gap-4 sm:flex-nowrap md:mb-3 md:gap-0"
actions={
<div className="flex flex-col xl:flex-row xl:space-x-5">
{!!data.connectedCalendars.length && (
<div className=" flex items-center">
<div className="flex items-center">
<AdditionalCalendarSelector isLoading={mutation.isLoading} />
</div>
)}
@@ -282,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";
@@ -16,7 +18,10 @@ function IntegrationListItem(props: {
children?: ReactNode;
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);
@@ -32,7 +37,8 @@ function IntegrationListItem(props: {
<ListItem
expanded={!!props.children}
className={classNames(
"my-0 flex-col rounded-md border transition-colors duration-500",
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
"my-0 flex-col border transition-colors duration-500 ",
highlight ? "bg-yellow-100" : ""
)}>
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
@@ -42,10 +48,19 @@ 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>
{props.children && <div className="w-full border-t border-gray-200">{props.children}</div>}
{props.children && <div className="w-full">{props.children}</div>}
</ListItem>
);
}
@@ -0,0 +1,63 @@
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton } from "@calcom/app-store/components";
import { classNames } from "@calcom/lib";
import useApp from "@calcom/lib/hooks/useApp";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui/Icon";
import { showToast } from "@calcom/ui/v2";
import Button from "@calcom/ui/v2/core/Button";
/**
* Use this component to allow installing an app from anywhere on the app.
* Use of this component requires you to remove custom InstallAppButtonComponent so that it can manage the redirection itself
*/
export default function OmniInstallAppButton({ appId, className }: { appId: string; className: string }) {
const { t } = useLocale();
const { data: app } = useApp(appId);
const utils = trpc.useContext();
const mutation = useAddAppMutation(null, {
onSuccess: () => {
//TODO: viewer.appById might be replaced with viewer.apps so that a single query needs to be invalidated.
utils.invalidateQueries(["viewer.appById", { appId }]);
utils.invalidateQueries(["viewer.apps", { extendsFeature: "EventType" }]);
showToast(t("app_successfully_installed"), "success");
},
onError: (error) => {
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
},
});
if (!app) {
return null;
}
return (
<InstallAppButton
type={app.type}
isProOnly={app.isProOnly}
wrapperClassName={classNames("[@media(max-width:260px)]:w-full", className)}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
onClick: () => {
mutation.mutate({ type: app.type, variant: app.variant, slug: app.slug, isOmniInstall: true });
},
};
}
return (
<Button
loading={mutation.isLoading}
color="secondary"
className="[@media(max-width:260px)]:w-full [@media(max-width:260px)]:justify-center"
StartIcon={Icon.FiPlus}
{...props}>
{t("install")}
</Button>
);
}}
/>
);
}
-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>
) : (
<></>
+32 -13
View File
@@ -4,19 +4,22 @@ 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;
date: Dayjs;
date?: Dayjs;
seatsPerTimeSlot?: number | null;
slots?: Slot[];
isLoading: boolean;
@@ -31,6 +34,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
eventTypeSlug,
recurringCount,
timeFormat,
onTimeFormatChange,
seatsPerTimeSlot,
ethSignature,
}) => {
@@ -44,18 +48,31 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
}, []);
if (!date) return null;
return (
<div className="dark:bg-darkgray-100 mt-8 flex flex-col px-4 text-center sm:mt-0 sm:w-1/3 sm:p-5 md:-mb-5">
<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="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-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="grid flex-grow grid-cols-1 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
<div className="-mb-5 grid flex-grow grid-cols-1 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
{slots.length > 0 &&
slots.map((slot) => {
type BookingURL = {
@@ -63,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(),
@@ -150,4 +167,6 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
);
};
AvailableTimes.displayName = "AvailableTimes";
export default AvailableTimes;
@@ -0,0 +1,107 @@
import { SchedulingType } from "@prisma/client";
import { FC, ReactNode } from "react";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui/Icon";
import { UserAvatars } from "@components/booking/UserAvatars";
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
import type { AvailabilityPageProps } from "../../pages/[user]/[type]";
import type { BookPageProps } from "../../pages/[user]/book";
import type { DynamicAvailabilityPageProps } from "../../pages/d/[link]/[slug]";
import type { HashLinkPageProps } from "../../pages/d/[link]/book";
import type { AvailabilityTeamPageProps } from "../../pages/team/[slug]/[type]";
import type { TeamBookingPageProps } from "../../pages/team/[slug]/book";
import { AvailableEventLocations } from "./AvailableEventLocations";
interface Props {
profile:
| AvailabilityPageProps["profile"]
| HashLinkPageProps["profile"]
| TeamBookingPageProps["profile"]
| BookPageProps["profile"]
| AvailabilityTeamPageProps["profile"]
| DynamicAvailabilityPageProps["profile"];
eventType:
| AvailabilityPageProps["eventType"]
| HashLinkPageProps["eventType"]
| TeamBookingPageProps["eventType"]
| BookPageProps["eventType"]
| AvailabilityTeamPageProps["eventType"]
| DynamicAvailabilityPageProps["eventType"];
isBookingPage?: boolean;
children: ReactNode;
isMobile?: boolean;
rescheduleUid?: string;
}
const BookingDescription: FC<Props> = (props) => {
const { profile, eventType, isBookingPage = false, children } = props;
const { t } = useLocale();
return (
<>
<UserAvatars
profile={profile}
users={eventType.users}
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
size={10}
truncateAfter={3}
/>
<h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
{profile.name}
</h2>
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold text-gray-900">
{eventType.title}
</h1>
<div className="dark:text-darkgray-600 flex flex-col space-y-3 text-sm font-medium text-gray-600">
{eventType?.description && (
<div
className={classNames(
"flex",
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
)}>
<div>
<Icon.FiInfo
className={classNames(
"mr-[10px] ml-[2px] inline-block h-4 w-4",
isBookingPage && "dark:text-darkgray-600 -mt-1 text-gray-500"
)}
/>
</div>
<div className="max-w-[calc(100%_-_2rem)] flex-shrink break-words">
<EventTypeDescriptionSafeHTML eventType={eventType} />
</div>
</div>
)}
{eventType?.requiresConfirmation && (
<div
className={classNames(
"flex items-center",
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
)}>
<div>
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
</div>
{t("requires_confirmation")}
</div>
)}
<AvailableEventLocations
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
/>
<p
className={classNames(
"text-sm font-medium",
isBookingPage && "dark:text-darkgray-600 text-gray-600"
)}>
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
{eventType.length} {t("minutes")}
</p>
{children}
</div>
</>
);
};
export default BookingDescription;
+86 -85
View File
@@ -1,20 +1,22 @@
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";
@@ -29,7 +31,7 @@ type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
type BookingItemProps = BookingItem & {
listingStatus: BookingListingStatus;
recurringBookings?: BookingItem[];
recurringBookings: inferQueryOutput<"viewer.bookings">["recurringInfo"];
};
function BookingListItem(booking: BookingItemProps) {
@@ -44,10 +46,25 @@ function BookingListItem(booking: BookingItemProps) {
const mutation = trpc.useMutation(["viewer.bookings.confirm"], {
onSuccess: () => {
setRejectionDialogIsOpen(false);
showToast(t("booking_confirmation_success"), "success");
utils.invalidateQueries("viewer.bookings");
},
onError: () => {
showToast(t("booking_confirmation_failed"), "error");
utils.invalidateQueries("viewer.bookings");
},
});
const isUpcoming = new Date(booking.endTime) >= new Date();
const isPast = new Date(booking.endTime) < new Date();
const isCancelled = booking.status === BookingStatus.CANCELLED;
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
const isRejected = booking.status === BookingStatus.REJECTED;
const isPending = booking.status === BookingStatus.PENDING;
const isRecurring = booking.recurringEventId !== null;
const isTabRecurring = booking.listingStatus === "recurring";
const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
const bookingConfirm = async (confirm: boolean) => {
let body = {
bookingId: booking.id,
@@ -56,28 +73,18 @@ function BookingListItem(booking: BookingItemProps) {
};
/**
* Only pass down the recurring event id when we need to confirm the entire series, which happens in
* the "Recurring" tab, to support confirming discretionally in the "Recurring" tab.
* the "Recurring" tab and "Unconfirmed" tab, to support confirming discretionally in the "Recurring" tab.
*/
if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) {
if ((isTabRecurring || isTabUnconfirmed) && isRecurring) {
body = Object.assign({}, body, { recurringEventId: booking.recurringEventId });
}
mutation.mutate(body);
};
const isUpcoming = new Date(booking.endTime) >= new Date();
const isPast = new Date(booking.endTime) < new Date();
const isCancelled = booking.status === BookingStatus.CANCELLED;
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
const isRejected = booking.status === BookingStatus.REJECTED;
const isPending = booking.status === BookingStatus.PENDING;
const pendingActions: ActionType[] = [
{
id: "reject",
label:
booking.listingStatus === "recurring" && booking.recurringEventId !== null
? t("reject_all")
: t("reject"),
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("reject_all") : t("reject"),
onClick: () => {
setRejectionDialogIsOpen(true);
},
@@ -86,10 +93,7 @@ function BookingListItem(booking: BookingItemProps) {
},
{
id: "confirm",
label:
booking.listingStatus === "recurring" && booking.recurringEventId !== null
? t("confirm_all")
: t("confirm"),
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
onClick: () => {
bookingConfirm(true);
},
@@ -102,17 +106,10 @@ function BookingListItem(booking: BookingItemProps) {
let bookedActions: ActionType[] = [
{
id: "cancel",
label:
booking.listingStatus === "recurring" && booking.recurringEventId !== null
? t("cancel_all_remaining")
: t("cancel"),
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
/* When cancelling we need to let the UI and the API know if the intention is to
cancel all remaining bookings or just that booking instance. */
href: `/cancel/${booking.uid}${
booking.listingStatus === "recurring" && booking.recurringEventId !== null
? "?allRemainingBookings=true"
: ""
}`,
href: `/cancel/${booking.uid}${isTabRecurring && isRecurring ? "?allRemainingBookings=true" : ""}`,
icon: Icon.FiX,
},
{
@@ -146,7 +143,7 @@ function BookingListItem(booking: BookingItemProps) {
},
];
if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) {
if (isTabRecurring && isRecurring) {
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
}
@@ -179,21 +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
let recurringStrings: string[] = [];
let recurringDates: Date[] = [];
if (booking.recurringBookings && booking.eventType.recurringEvent?.freq !== undefined) {
[recurringStrings, recurringDates] = extractRecurringDates(
booking.recurringBookings,
user?.timeZone,
i18n
);
}
// @FIXME: This is importing the RRULE library which is already heavy. Find out a more optimal way do this.
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: {
@@ -201,7 +197,7 @@ function BookingListItem(booking: BookingItemProps) {
// TODO: Booking when fetched should have id 0 already(for Dynamic Events).
type: booking.eventType.id || 0,
eventSlug: booking.eventType.slug,
user: user?.username || "",
username: user?.username || "",
name: booking.attendees[0] ? booking.attendees[0].name : undefined,
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
location: location,
@@ -214,6 +210,7 @@ function BookingListItem(booking: BookingItemProps) {
},
});
};
return (
<>
<RescheduleDialog
@@ -262,12 +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:w-48" 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(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
{dayjs(booking.endTime).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 && (
@@ -288,22 +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={"flex-1 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(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
{dayjs(booking.endTime).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>
@@ -323,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>
@@ -335,7 +340,7 @@ function BookingListItem(booking: BookingItemProps) {
<div
title={booking.title}
className={classNames(
"max-w-10/12 sm:max-w-56 truncate text-sm font-medium leading-6 text-neutral-900 md:max-w-max",
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-neutral-900 md:max-w-full",
isCancelled ? "line-through" : ""
)}>
{booking.title}
@@ -347,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>
@@ -365,20 +370,8 @@ function BookingListItem(booking: BookingItemProps) {
</div>
)}
</div>
<div className="flex justify-end sm:hidden">
<div className="whitespace-nowrap pt-2 pb-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4">
{isUpcoming && !isCancelled ? (
<>
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
{isConfirmed && <TableActions actions={bookedActions} />}
{isRejected && <div className="text-sm text-gray-500">{t("rejected")}</div>}
</>
) : null}
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
</div>
</div>
</td>
<td className="hidden whitespace-nowrap py-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:block">
<td className="py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:pl-0">
{isUpcoming && !isCancelled ? (
<>
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
@@ -400,26 +393,34 @@ 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 &&
booking.eventType?.recurringEvent?.freq &&
(booking.listingStatus === "recurring" || booking.listingStatus === "cancelled") && (
(booking.listingStatus === "recurring" ||
booking.listingStatus === "unconfirmed" ||
booking.listingStatus === "cancelled") && (
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
<div className="flex">
<Tooltip
content={recurringStrings.map((aDate, key) => (
<p key={key}>{aDate}</p>
content={recurringDates.map((aDate, key) => (
<p key={key} className={classNames(recurringDates[key] < now && "line-through")}>
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)}
{" - "}
{dayjs(aDate).format("D MMMM YYYY")}
</p>
))}>
<div className="text-gray-600 dark:text-white">
<Icon.FiRefreshCcw
@@ -429,12 +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: booking.recurringBookings.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());
+4 -24
View File
@@ -1,24 +1,19 @@
import { FC, useEffect, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import TimezoneSelect, { ITimezoneOption } from "@calcom/ui/form/TimezoneSelect";
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;
};
const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone, timeFormat }) => {
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());
}, []);
@@ -27,26 +22,11 @@ const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone, timeFormat
onSelectTimeZone(timeZone(selectedTimeZone));
}
}, [selectedTimeZone, onSelectTimeZone]);
const handle24hClockToggle = (is24hClock: boolean) => {
setIs24hClock(is24hClock);
onToggle24hClock(is24h(is24hClock));
};
return selectedTimeZone !== "" ? (
<div className="max-w-80 dark:border-darkgray-300 dark:bg-darkgray-200 absolute z-10 w-full rounded-sm border border-gray-200 bg-white px-4 pt-4 pb-3 shadow-sm">
<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>
<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,7 +1,6 @@
// Get router variables
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { EventType } from "@prisma/client";
import { SchedulingType } from "@prisma/client";
import * as Collapsible from "@radix-ui/react-collapsible";
import * as Popover from "@radix-ui/react-popover";
import { TFunction } from "next-i18next";
import { useRouter } from "next/router";
import { useReducer, useEffect, useMemo, useState } from "react";
@@ -9,6 +8,8 @@ import { Toaster } from "react-hot-toast";
import { FormattedNumber, IntlProvider } from "react-intl";
import { z } from "zod";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import { getEventTypeAppData } from "@calcom/app-store/utils";
import dayjs, { Dayjs } from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
@@ -19,13 +20,13 @@ import {
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import { WEBSITE_URL } from "@calcom/lib/constants";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import notEmpty from "@calcom/lib/notEmpty";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage";
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";
@@ -37,17 +38,16 @@ import { isBrandingHidden } from "@lib/isBrandingHidden";
import Gates, { Gate, GateState } from "@components/Gates";
import AvailableTimes from "@components/booking/AvailableTimes";
import BookingDescription from "@components/booking/BookingDescription";
import TimeOptions from "@components/booking/TimeOptions";
import { UserAvatars } from "@components/booking/UserAvatars";
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
import { HeadSeo } from "@components/seo/head-seo";
import PoweredByCal from "@components/ui/PoweredByCal";
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
import { AvailableEventLocations } from "../AvailableEventLocations";
// Get router variables
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
const router = useRouter();
const path = router.asPath.split("/");
@@ -79,7 +79,7 @@ const useSlots = ({
usernameList: string[];
timeZone?: string;
}) => {
const { data, isLoading, isIdle } = trpc.useQuery(
const { data, isLoading, isPaused } = trpc.useQuery(
[
"viewer.public.slots.getSchedule",
{
@@ -101,13 +101,14 @@ const useSlots = ({
}
}, [data]);
// The very first time isIdle is set if auto-fetch is disabled, so isIdle should also be considered a loading state.
return { slots: cachedSlots, isLoading: isLoading || isIdle };
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
return { slots: cachedSlots, isLoading: isLoading || isPaused };
};
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;
@@ -130,6 +132,8 @@ const SlotPicker = ({
const { month, setQuery: setMonth } = useRouterQuery("month");
const router = useRouter();
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
useEffect(() => {
if (!router.isReady) return;
@@ -176,10 +180,8 @@ const SlotPicker = ({
<DatePicker
isLoading={isLoading}
className={classNames(
"mt-8 w-full px-4 sm:mt-0 sm:min-w-[455px] md:px-5",
selectedDate
? "sm:dark:border-darkgray-200 border-gray-200 sm:w-1/2 sm:border-r sm:p-4 sm:pr-6 md:w-1/3"
: "sm:p-4"
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 lg:min-w-[455px]",
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
)}
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
locale={isLocaleReady ? i18n.language : "en"}
@@ -194,55 +196,42 @@ const SlotPicker = ({
weekStart={weekStart}
/>
{selectedDate && (
<div ref={slotPickerRef}>
<AvailableTimes
isLoading={isLoading}
slots={slots[selectedDate.format("YYYY-MM-DD")]}
slots={selectedDate && slots[selectedDate.format("YYYY-MM-DD")]}
date={selectedDate}
timeFormat={timeFormat}
onTimeFormatChange={onTimeFormatChange}
eventTypeId={eventType.id}
eventTypeSlug={eventType.slug}
seatsPerTimeSlot={seatsPerTimeSlot}
recurringCount={recurringEventCount}
ethSignature={ethSignature}
/>
)}
</div>
</>
);
};
function TimezoneDropdown({
onChangeTimeFormat,
onChangeTimeZone,
timeZone,
timeFormat,
}: {
onChangeTimeFormat: (newTimeFormat: string) => void;
onChangeTimeZone: (newTimeZone: string) => void;
timeZone?: string;
timeFormat: string;
}) {
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
useEffect(() => {
handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true");
// 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 (
<Collapsible.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen} className="flex">
<Collapsible.Trigger className="min-w-32 dark:text-darkgray-600 mb-2 -ml-2 px-2 text-left text-gray-600">
<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">
<p className="text-sm font-medium">
<Icon.FiGlobe className="mr-[10px] ml-[2px] -mt-[2px] inline-block h-4 w-4" />
{timeZone}
@@ -252,15 +241,16 @@ function TimezoneDropdown({
<Icon.FiChevronDown className="ml-1 inline-block h-4 w-4" />
)}
</p>
</Collapsible.Trigger>
<Collapsible.Content>
<TimeOptions
onSelectTimeZone={handleSelectTimeZone}
onToggle24hClock={handleToggle24hClock}
timeFormat={timeFormat}
/>
</Collapsible.Content>
</Collapsible.Root>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
hideWhenDetached
align="start"
className="animate-fade-in-up absolute left-0 top-2 w-80 max-w-[calc(100vw_-_1.5rem)]">
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}
@@ -289,9 +279,9 @@ const useRouterQuery = <T extends string>(name: T) => {
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
const AvailabilityPage = ({ profile, eventType }: Props) => {
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);
@@ -302,8 +292,13 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
const isBackgroundTransparent = useIsBackgroundTransparent();
const [timeZone, setTimeZone] = useState<string>();
const [timeFormat, setTimeFormat] = useState(detectBrowserTimeFormat);
const [isAvailableTimesVisible, setIsAvailableTimesVisible] = useState<boolean>();
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>) => ({
...state,
@@ -316,10 +311,6 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
}, []);
useEffect(() => {
setIsAvailableTimesVisible(!!query.date);
}, [query.date]);
// TODO: Improve this;
useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO");
@@ -338,248 +329,118 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
// get dynamic user list here
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
// Recurring event sidebar requires more space
const maxWidth = isAvailableTimesVisible
? recurringEventCount
? "max-w-6xl"
: "max-w-5xl"
: recurringEventCount
? "max-w-4xl"
: "max-w-3xl";
const timezoneDropdown = useMemo(
() => (
<TimezoneDropdown
timeFormat={timeFormat}
onChangeTimeFormat={setTimeFormat}
timeZone={timeZone}
onChangeTimeZone={setTimeZone}
/>
),
[timeZone, timeFormat]
() => <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 = [
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
eventType.metadata && eventType.metadata.blockchainId && eventType.metadata.smartContractAddress
rainbowAppData && rainbowAppData.blockchainId && rainbowAppData.smartContractAddress
? ("rainbow" as Gate)
: undefined,
];
return (
<Gates gates={gates} metadata={eventType.metadata} dispatch={gateDispatcher}>
<Gates gates={gates} appData={rainbowAppData} dispatch={gateDispatcher}>
<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,
}}
/>
<BookingPageTagManager eventType={eventType} />
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<div>
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed
? classNames(maxWidth)
: classNames("transition-max-width mx-auto my-0 duration-500 ease-in-out md:my-24", maxWidth)
"flex-col md:mx-4 lg:flex",
shouldAlignCentrally ? "items-center" : "items-start",
!isEmbed && classNames("mx-auto my-0 ease-in-out md:my-24")
)}>
<div
style={availabilityDatePickerEmbedStyles}
className={classNames(
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white",
"border-bookinglightest overflow-hidden rounded-md md:border",
isEmbed ? "mx-auto" : maxWidth
)}>
{/* mobile: details */}
<div className="block px-4 pt-4 sm:p-8 md:hidden">
<div>
<UserAvatars
profile={profile}
users={eventType.users}
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
size={9}
truncateAfter={5}
/>
<div className="mt-4">
<div className="dark:text-darkgray-900 mt-2 gap-2">
<p className="break-words text-sm font-medium text-gray-600 dark:text-gray-300">
{profile.name}
</p>
<h1 className="text-bookingdark dark:text-darkgray-900 mb-4 break-words text-xl font-semibold">
{eventType.title}
</h1>
<div className="flex flex-col space-y-3">
{eventType?.description && (
<div className="dark:text-darkgray-600 flex py-1 text-sm font-medium text-gray-600">
<div>
<Icon.FiInfo className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
</div>
<EventTypeDescriptionSafeHTML eventType={eventType} />
<div>
<div
style={availabilityDatePickerEmbedStyles}
className={classNames(
isBackgroundTransparent
? ""
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
"border-bookinglightest overflow-hidden md:rounded-md md:border",
isEmbed && "mx-auto"
)}>
<div className="overflow-hidden md:flex">
<div
className={classNames(
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
"min-w-full md:w-[230px] md:min-w-[230px]",
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
)}>
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
{!rescheduleUid && eventType.recurringEvent && (
<div className="flex items-start text-sm font-medium">
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-[7px] ml-[2px] inline-block h-4 w-4 " />
<div>
<p className="mb-1 -ml-2 inline px-2 py-1">
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
</p>
<input
type="number"
min="1"
max={eventType.recurringEvent.count}
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
defaultValue={eventType.recurringEvent.count}
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
}}
/>
<p className="inline">
{t("occurrence", {
count: recurringEventCount,
})}
</p>
</div>
)}
{eventType?.requiresConfirmation && (
<p className="dark:text-darkgray-600 text-gray-600">
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
{t("requires_confirmation")}
</p>
)}
<AvailableEventLocations locations={eventType.locations} />
<p className="dark:text-darkgray-600 text-gray-600">
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
{eventType.length} {t("minutes")}
</div>
)}
{stripeAppData.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium">
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
<IntlProvider locale="en">
<FormattedNumber
value={stripeAppData.price / 100.0}
style="currency"
currency={stripeAppData.currency.toUpperCase()}
/>
</IntlProvider>
</p>
{eventType.price > 0 && (
<div className="dark:text-darkgray-600 text-gray-600">
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
<IntlProvider locale="en">
<FormattedNumber
value={eventType.price / 100.0}
style="currency"
currency={eventType.currency.toUpperCase()}
/>
</IntlProvider>
</div>
)}
{!rescheduleUid && eventType.recurringEvent && (
<div className="dark:text-darkgray-600 flex items-center text-gray-600">
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-1 ml-[2px] inline-block h-4 w-4 shrink-0 text-gray-500" />
<div>
<p className="mb-1 -ml-2 inline px-2 py-1">
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
</p>
<input
type="number"
min="1"
max={eventType.recurringEvent.count}
className="w-15 dark:text-darkgray-600 h-7 rounded-sm border-gray-300 bg-white text-sm text-gray-600 [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600"
defaultValue={eventType.recurringEvent.count}
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
}}
/>
<p className="dark:text-darkgray-600 inline text-gray-600">
{t("occurrence", {
count: recurringEventCount,
})}
</p>
</div>
</div>
)}
{timezoneDropdown}
)}
{timezoneDropdown}
</BookingDescription>
<div className="md:hidden">
{/* Temp disabled booking?.startTime && rescheduleUid && (
<div>
<p
className="mt-8 text-gray-600 dark:text-darkgray-600"
data-testid="former_time_p_mobile">
{t("former_time")}
</p>
<p className="text-gray-500 line-through dark:text-darkgray-600">
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
{typeof booking.startTime === "string" &&
parseDate(dayjs(booking.startTime), i18n)}
</p>
</div>
)*/}
</div>
</div>
</div>
</div>
</div>
</div>
<div className="overflow-hidden sm:flex">
<div
className={
"sm:dark:border-darkgray-200 hidden overflow-hidden border-gray-200 p-5 sm:border-r md:flex md:flex-col " +
(isAvailableTimesVisible ? "sm:w-1/3" : recurringEventCount ? "sm:w-2/3" : "sm:w-1/2")
}>
<UserAvatars
profile={profile}
users={eventType.users}
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
size={10}
truncateAfter={3}
/>
<h2 className="break-words text-sm font-medium text-gray-600 dark:text-gray-300 lg:mt-2">
{profile.name}
</h2>
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl text-gray-900">
{eventType.title}
</h1>
<div className="dark:text-darkgray-600 flex flex-col space-y-3 text-sm font-medium text-gray-600">
{eventType?.description && (
<div className="flex">
<div>
<Icon.FiInfo className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
</div>
<EventTypeDescriptionSafeHTML eventType={eventType} />
{!isEmbed && (
<div className="mt-auto hidden md:block">
<GoBackToPreviousPage t={t} />
</div>
)}
{eventType?.requiresConfirmation && (
<div className="flex items-center">
<div>
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
</div>
{t("requires_confirmation")}
</div>
)}
<AvailableEventLocations locations={eventType.locations} />
<p className="text-sm font-medium">
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
{eventType.length} {t("minutes")}
</p>
{!rescheduleUid && eventType.recurringEvent && (
<div className="flex items-start text-sm font-medium">
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-[7px] ml-[2px] inline-block h-4 w-4 " />
<div>
<p className="mb-1 -ml-2 inline px-2 py-1">
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
</p>
<input
type="number"
min="1"
max={eventType.recurringEvent.count}
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
defaultValue={eventType.recurringEvent.count}
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
}}
/>
<p className="inline">
{t("occurrence", {
count: recurringEventCount,
})}
</p>
</div>
</div>
)}
{eventType.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium">
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
<IntlProvider locale="en">
<FormattedNumber
value={eventType.price / 100.0}
style="currency"
currency={eventType.currency.toUpperCase()}
/>
</IntlProvider>
</p>
)}
{timezoneDropdown}
</div>
{!isEmbed && <GoBackToPreviousPage t={t} />}
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
<div>
<p
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
@@ -592,26 +453,34 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
</p>
</div>
)*/}
</div>
<SlotPicker
weekStart={
typeof profile.weekStart === "string"
? ([
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
].indexOf(profile.weekStart) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
: profile.weekStart /* Allows providing weekStart as number */
}
eventType={eventType}
timeFormat={timeFormat}
onTimeFormatChange={onTimeFormatChange}
timeZone={timeZone}
users={userList}
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
recurringEventCount={recurringEventCount}
ethSignature={gateState.rainbowToken}
/>
</div>
<SlotPicker
weekStart={
typeof profile.weekStart === "string"
? (["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
profile.weekStart
) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
: profile.weekStart /* Allows providing weekStart as number */
}
eventType={eventType}
timeFormat={timeFormat}
timeZone={timeZone}
users={userList}
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
recurringEventCount={recurringEventCount}
ethSignature={gateState.rainbowToken}
/>
</div>
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && <PoweredByCal />}
</div>
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && <PoweredByCal />}
</main>
</div>
<Toaster position="bottom-right" />
+132 -112
View File
@@ -1,6 +1,6 @@
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";
import Head from "next/head";
@@ -9,17 +9,19 @@ import { useEffect, useMemo, useState, useReducer } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { FormattedNumber, IntlProvider } from "react-intl";
import { ReactMultiEmail } from "react-multi-email";
import { useMutation } from "react-query";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import {
locationKeyToString,
getEventLocationValue,
getEventLocationType,
EventLocationType,
getHumanReadableLocationValue,
} from "@calcom/app-store/locations";
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import { getEventTypeAppData } from "@calcom/app-store/utils";
import { LocationObject, LocationType } from "@calcom/core/location";
import dayjs from "@calcom/dayjs";
import {
@@ -29,6 +31,8 @@ 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";
import { HttpError } from "@calcom/lib/http-error";
@@ -43,14 +47,14 @@ 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";
import slugify from "@lib/slugify";
import Gates, { Gate, GateState } from "@components/Gates";
import { UserAvatars } from "@components/booking/UserAvatars";
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
import BookingDescription from "@components/booking/BookingDescription";
import { BookPageProps } from "../../../pages/[user]/book";
import { HashLinkPageProps } from "../../../pages/d/[link]/book";
@@ -81,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();
@@ -97,6 +105,7 @@ const BookingPage = ({
}),
{}
);
const stripeAppData = getStripeAppData(eventType);
useEffect(() => {
if (top !== window) {
@@ -111,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,
})
);
@@ -130,10 +139,10 @@ const BookingPage = ({
date,
type: eventType.id,
eventSlug: eventType.slug,
user: profile.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,
@@ -163,7 +172,7 @@ const BookingPage = ({
type: eventType.id,
eventSlug: eventType.slug,
recur: recurringEventId,
user: profile.slug,
username: profile.slug,
reschedule: !!rescheduleUid,
name: attendees[0].name,
email: attendees[0].email,
@@ -258,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();
@@ -354,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);
@@ -382,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,
});
}
@@ -396,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) => {
@@ -403,23 +419,25 @@ const BookingPage = ({
workflowReference.workflow.steps.forEach((step) => {
if (step.action === WorkflowActions.SMS_ATTENDEE) {
isSmsReminderNumberNeeded = true;
isSmsReminderNumberRequired = step.numberRequired || false;
return;
}
});
}
});
}
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
// Define conditional gates here
const gates = [
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
eventType.metadata && eventType.metadata.blockchainId && eventType.metadata.smartContractAddress
rainbowAppData && rainbowAppData.blockchainId && rainbowAppData.smartContractAddress
? ("rainbow" as Gate)
: undefined,
];
return (
<Gates gates={gates} metadata={eventType.metadata} dispatch={gateDispatcher}>
<Gates gates={gates} appData={rainbowAppData} dispatch={gateDispatcher}>
<Head>
<title>
{rescheduleUid
@@ -435,6 +453,7 @@ const BookingPage = ({
</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<BookingPageTagManager eventType={eventType} />
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<main
className={classNames(
@@ -445,54 +464,20 @@ const BookingPage = ({
<div
className={classNames(
"main overflow-hidden",
isEmbed ? "" : "border border-gray-200",
isBackgroundTransparent ? "" : "dark:border-1 dark:bg-darkgray-100 bg-white",
"dark:border-darkgray-300 rounded-md sm:border"
)}>
<div className="sm:flex">
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
<UserAvatars
profile={profile}
users={eventType.users}
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
size={10}
truncateAfter={3}
/>
<h2 className="mt-2 break-words text-sm font-medium text-gray-500 dark:text-gray-300">
{profile.name}
</h2>
<h1 className="font-cal dark:text-darkgray-900 break-words text-2xl text-gray-900 ">
{eventType.title}
</h1>
<div className="mt-4 flex flex-col space-y-3 lg:mt-9">
{eventType?.description && (
<div className="dark:text-darkgray-600 flex text-sm font-medium text-gray-600">
<div>
<Icon.FiInfo className="dark:text-darkgray-600 mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
</div>
<EventTypeDescriptionSafeHTML eventType={eventType} />
</div>
)}
{eventType?.requiresConfirmation && (
<div className="dark:text-darkgray-600 flex items-center text-sm font-medium text-gray-600">
<div>
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
</div>
{t("requires_confirmation")}
</div>
)}
<p className="dark:text-darkgray-600 text-sm font-medium text-gray-600">
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
{eventType.length} {t("minutes")}
</p>
{eventType.price > 0 && (
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
{stripeAppData.price > 0 && (
<p className="text-bookinglight -ml-2 px-2 text-sm ">
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
<IntlProvider locale="en">
<FormattedNumber
value={eventType.price / 100.0}
value={stripeAppData.price / 100.0}
style="currency"
currency={eventType.currency.toUpperCase()}
currency={stripeAppData.currency.toUpperCase()}
/>
</IntlProvider>
</p>
@@ -513,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 })}
@@ -544,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
@@ -560,7 +563,7 @@ const BookingPage = ({
</p>
</div>
)}
</div>
</BookingDescription>
</div>
<div className="p-6 sm:w-1/2">
<Form form={bookingForm} handleSubmit={bookEvent}>
@@ -605,37 +608,51 @@ const BookingPage = ({
)}
</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);
// TODO: Right now selectedLocationType isn't send by getSSP. Once that's available defaultChecked should work and show the location in the original booking
const defaultChecked = rescheduleUid ? selectedLocationType === location.type : i === 0;
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={defaultChecked}
/>
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-white">
{locationKeyToString(location)}
</span>
</label>
);
})}
</div>
)}
<>
{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>
</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">
@@ -713,23 +730,25 @@ const BookingPage = ({
/>
)}
{input.type === EventTypeCustomInputType.BOOL && (
<div className="flex h-5 items-center">
<input
type="checkbox"
{...bookingForm.register(`customInputs.${input.id}`, {
required: input.required,
})}
required={input.required}
id={"custom_" + input.id}
className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black disabled:bg-gray-200 ltr:mr-2 rtl:ml-2 disabled:dark:text-gray-500"
placeholder=""
disabled={disabledExceptForOwner}
/>
<label
htmlFor={"custom_" + input.id}
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
{input.label}
</label>
<div className="my-6">
<div className="flex">
<input
type="checkbox"
{...bookingForm.register(`customInputs.${input.id}`, {
required: input.required,
})}
required={input.required}
id={"custom_" + input.id}
className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black disabled:bg-gray-200 ltr:mr-2 rtl:ml-2 disabled:dark:text-gray-500"
placeholder=""
disabled={disabledExceptForOwner}
/>
<label
htmlFor={"custom_" + input.id}
className="-mt-px block text-sm font-medium text-gray-700 dark:text-white">
{input.label}
</label>
</div>
</div>
)}
</div>
@@ -793,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>
@@ -801,7 +820,7 @@ const BookingPage = ({
name="smsReminderNumber"
placeholder={t("enter_phone_number")}
id="smsReminderNumber"
required
required={isSmsReminderNumberRequired}
/>
</div>
{bookingForm.formState.errors.smsReminderNumber && (
@@ -830,6 +849,7 @@ const BookingPage = ({
) : (
<textarea
{...bookingForm.register("notes")}
required={!!eventType.metadata.additionalNotesRequired}
id="notes"
name="notes"
rows={3}
+15 -33
View File
@@ -1,16 +1,12 @@
import { RescheduleResponse } from "pages/api/book/request-reschedule";
import React, { useState, Dispatch, SetStateAction } from "react";
import { useMutation } from "react-query";
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,89 +0,0 @@
import { Prisma, SchedulingType } from "@prisma/client";
import { useMemo } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { parseRecurringEvent } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { Icon } from "@calcom/ui/Icon";
import classNames from "@lib/classNames";
const eventTypeData = Prisma.validator<Prisma.EventTypeArgs>()({
select: baseEventTypeSelect,
});
type EventType = Prisma.EventTypeGetPayload<typeof eventTypeData>;
export type EventTypeDescriptionProps = {
eventType: EventType;
className?: string;
};
export const EventTypeDescription = ({ eventType, className }: EventTypeDescriptionProps) => {
const { t } = useLocale();
const recurringEvent = useMemo(
() => parseRecurringEvent(eventType.recurringEvent),
[eventType.recurringEvent]
);
return (
<>
<div className={classNames("text-gray-600 dark:text-white", className)}>
{eventType.description && (
<h2 className="max-w-[280px] overflow-hidden text-ellipsis sm:max-w-[500px]">
{eventType.description.substring(0, 100)}
{eventType.description.length > 100 && "..."}
</h2>
)}
<ul className="mt-2 flex flex-wrap space-x-1 sm:flex-nowrap ">
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
<Icon.FiClock className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
{eventType.length} {t("minutes")}
</li>
{eventType.schedulingType ? (
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
<Icon.FiUsers className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && t("round_robin")}
{eventType.schedulingType === SchedulingType.COLLECTIVE && t("collective")}
</li>
) : (
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
<Icon.FiUser className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
{t("1_on_1")}
</li>
)}
{recurringEvent?.count && recurringEvent.count > 0 && (
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
<Icon.FiRefreshCw className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
{t("repeats_up_to", {
count: recurringEvent.count,
})}
</li>
)}
{eventType.price > 0 && (
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
<Icon.FiCreditCard className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
<IntlProvider locale="en">
<FormattedNumber
value={eventType.price / 100.0}
style="currency"
currency={eventType.currency.toUpperCase()}
/>
</IntlProvider>
</li>
)}
{eventType.requiresConfirmation && (
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
<Icon.FiCheckSquare className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
{t("requires_confirmation")}
</li>
)}
</ul>
</div>
</>
);
};
export default EventTypeDescription;
@@ -1,145 +0,0 @@
import type { FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { UseFormReturn } from "react-hook-form";
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 Select from "@components/ui/form/Select";
type RecurringEventControllerProps = {
recurringEvent: RecurringEvent | null;
formMethods: UseFormReturn<FormValues>;
paymentEnabled: boolean;
onRecurringEventDefined: (value: boolean) => void;
};
/**
* @deprecated
* use component from `/apps/web/components/v2/eventtype/RecurringEventController` instead
**/
export default function RecurringEventController({
recurringEvent,
formMethods,
paymentEnabled,
onRecurringEventDefined,
}: RecurringEventControllerProps) {
const { t } = useLocale();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(recurringEvent);
/* Just yearly-0, monthly-1 and weekly-2 */
const recurringEventFreqOptions = Object.entries(Frequency)
.filter(([key, value]) => isNaN(Number(key)) && Number(value) < 3)
.map(([key, value]) => ({
label: t(`${key.toString().toLowerCase()}`, { count: recurringEventState?.interval }),
value: value.toString(),
}));
return (
<div className="block items-start sm:flex">
<div className="min-w-48 mb-4 sm:mb-0">
<span className="flex text-sm font-medium text-neutral-700">{t("recurring_event")}</span>
</div>
<div className={!paymentEnabled ? "w-full" : ""}>
{paymentEnabled ? (
<Alert severity="warning" title={t("warning_payment_recurring_event")} />
) : (
<>
<div className="flex items-start">
<div className="flex h-5 items-start">
<input
onChange={(event) => {
onRecurringEventDefined(event?.target.checked);
if (!event?.target.checked) {
formMethods.setValue("recurringEvent", null);
setRecurringEventState(null);
} else {
const newVal = recurringEvent || {
interval: 1,
count: 12,
freq: Frequency.WEEKLY,
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}
}}
type="checkbox"
className="text-primary-600 h-4 w-4 rounded border-gray-300"
defaultChecked={recurringEventState !== null}
data-testid="recurring-event-check"
id="recurringEvent"
/>
</div>
<div className="text-sm ltr:ml-3 rtl:mr-3">
<label htmlFor="recurringEvent" className="text-neutral-900">
{t("recurring_event_description")}
</label>
</div>
</div>
{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 w-16 rounded-sm 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 min-w-0 rounded-sm 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("max")}</p>
<input
type="number"
min="1"
max="20"
className="block w-16 rounded-sm 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("occurrence", {
count: recurringEventState.count,
})}
</p>
</div>
</div>
)}
</>
)}
</div>
</div>
);
}
@@ -14,10 +14,11 @@ const CalendarItem = (props: ICalendarItem) => {
const { title, imageSrc, type } = props;
const { t } = useLocale();
return (
<div className="flex flex-row items-center p-5">
<img src={imageSrc} alt={title} className="h-8 w-8" />
<p className="mx-3 text-sm font-bold">{title}</p>
<div className="flex flex-row items-center justify-between p-5">
<div className="flex items-center space-x-3">
<img src={imageSrc} alt={title} className="h-8 w-8" />
<p className="text-sm font-bold">{title}</p>
</div>
<InstallAppButtonWithoutPlanCheck
type={type}
render={(buttonProps) => (
@@ -29,8 +30,7 @@ const CalendarItem = (props: ICalendarItem) => {
// Save cookie key to return url step
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
}}
className="ml-auto rounded-md border border-gray-200 py-[10px] px-4 text-sm font-bold">
}}>
{t("connect")}
</Button>
)}
@@ -1,8 +1,8 @@
import { useMutation } from "react-query";
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>
@@ -76,7 +76,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
</List>
)}
{queryConnectedCalendars.isLoading && (
{queryIntegrations.isLoading && (
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white p-0 dark:bg-black">
{[0, 0, 0, 0].map((_item, index) => {
return (
@@ -149,7 +149,7 @@ const UserProfile = (props: IUserProfile) => {
}}
/>
{errors.bio && (
<p data-testid="required" className="text-xs italic text-red-500">
<p data-testid="required" className="py-2 text-xs text-red-500">
{t("required")}
</p>
)}
@@ -85,7 +85,7 @@ const UserSettings = (props: IUserSettingsProps) => {
className="w-full rounded-md border border-gray-300 text-sm"
/>
{errors.name && (
<p data-testid="required" className="text-xs italic text-red-500">
<p data-testid="required" className="py-2 text-xs text-red-500">
{t("required")}
</p>
)}
@@ -1,9 +1,8 @@
import { useMutation } from "@tanstack/react-query";
import { Fragment } from "react";
import { useMutation } from "react-query";
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 { useMutation } from "react-query";
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";
+34 -37
View File
@@ -1,10 +1,7 @@
import Link from "next/link";
import { TeamPageProps } from "pages/team/[slug]";
import React from "react";
import { WEBAPP_URL } from "@calcom/lib/constants";
import Button from "@calcom/ui/Button";
import { Icon } from "@calcom/ui/Icon";
import { Avatar } from "@calcom/ui/v2";
import { useLocale } from "@lib/hooks/useLocale";
@@ -13,46 +10,46 @@ type TeamType = TeamPageProps["team"];
type MembersType = TeamType["members"];
type MemberType = MembersType[number];
const Team = ({ team }: TeamPageProps) => {
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
const { t } = useLocale();
const Member = ({ member }: { member: MemberType }) => {
return (
<Link key={member.id} href={`/${member.username}`}>
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex h-full flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 ">
<Avatar
size="md"
alt={member.name || ""}
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
/>
<section className="line-clamp-4 mt-2 w-full space-y-1">
<p className="font-medium text-neutral-900 dark:text-white">{member.name}</p>
<p className="line-clamp-3 overflow-ellipsis text-sm font-normal text-neutral-500 dark:text-white">
{member.bio || t("user_from_team", { user: member.name, team: team.name })}
</p>
</section>
</div>
</Link>
);
};
return (
<Link key={member.id} href={`/${member.username}`}>
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full w-[90%] flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 ">
<Avatar
size="md"
alt={member.name || ""}
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
/>
<section className="line-clamp-4 mt-2 w-full space-y-1">
<p className="font-medium text-neutral-900 dark:text-white">{member.name}</p>
<p className="line-clamp-3 overflow-ellipsis text-sm font-normal text-neutral-500 dark:text-white">
{member.bio || t("user_from_team", { user: member.name, team: teamName })}
</p>
</section>
</div>
</Link>
);
};
const Members = ({ members }: { members: MembersType }) => {
if (!members || members.length === 0) {
return null;
}
return (
<section className="lg:min-w-lg mx-auto flex min-w-full max-w-5xl flex-wrap justify-center gap-x-6 gap-y-6">
{members.map((member) => {
return member.username !== null && <Member key={member.id} member={member} />;
})}
</section>
);
};
const Members = ({ members, teamName }: { members: MembersType; teamName: string | null }) => {
if (!members || members.length === 0) {
return null;
}
return (
<section className="lg:min-w-lg mx-auto flex min-w-full max-w-5xl flex-wrap justify-center gap-x-6 gap-y-6">
{members.map((member) => {
return member.username !== null && <Member key={member.id} member={member} teamName={teamName} />;
})}
</section>
);
};
const Team = ({ team }: TeamPageProps) => {
return (
<div>
<Members members={team.members} />
<Members members={team.members} teamName={team.name} />
</div>
);
};
+30 -32
View File
@@ -1,50 +1,48 @@
import { useState } from "react";
import classNames from "classnames";
import React, { useState } from "react";
import { ControllerRenderProps } from "react-hook-form";
import { Icon } from "@calcom/ui/Icon";
const EditableHeading = ({
title,
const EditableHeading = function EditableHeading({
value,
onChange,
placeholder = "",
readOnly = false,
isReady,
...passThroughProps
}: {
title: string;
onChange?: (value: string) => void;
placeholder?: string;
readOnly?: boolean;
}) => {
isReady?: boolean;
} & Omit<JSX.IntrinsicElements["input"], "name" | "onChange"> &
ControllerRenderProps) {
const [isEditing, setIsEditing] = useState(false);
const enableEditing = () => !readOnly && setIsEditing(true);
const enableEditing = () => setIsEditing(true);
return (
<div className="group relative cursor-pointer" onClick={enableEditing}>
{!isEditing ? (
<>
<h1
style={{ fontSize: 22, letterSpacing: "-0.0009em" }}
className="inline pl-0 normal-case text-gray-900 focus:text-black group-hover:text-gray-500">
{title}
</h1>
{!readOnly ? (
<Icon.FiEdit2 className="ml-1 -mt-1 inline h-4 w-4 text-gray-700 group-hover:text-gray-500" />
) : null}
</>
) : (
<div style={{ marginBottom: -11 }}>
<div className="flex items-center">
<label className="min-w-8 relative inline-block">
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value}&nbsp;</span>
{!isEditing && isReady && (
<Icon.FiEdit2 className="ml-1 inline h-4 w-4 align-top text-gray-700 group-hover:text-gray-500" />
)}
<input
{...passThroughProps}
type="text"
autoFocus
style={{ top: -6, fontSize: 22 }}
value={value}
required
className="relative h-10 w-full cursor-pointer border-none bg-transparent pl-0 text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0"
placeholder={placeholder}
defaultValue={title}
className={classNames(
"absolute top-0 left-0 w-full cursor-pointer border-none bg-transparent p-0 align-top text-xl text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0"
)}
onFocus={(e) => {
setIsEditing(true);
passThroughProps.onFocus && passThroughProps.onFocus(e);
}}
onBlur={(e) => {
setIsEditing(false);
onChange && onChange(e.target.value);
passThroughProps.onBlur && passThroughProps.onBlur(e);
}}
onChange={(e) => onChange && onChange(e.target.value)}
/>
</div>
)}
</label>
</div>
</div>
);
};
+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>
);
};
+25 -17
View File
@@ -1,6 +1,11 @@
import React, { FC } from "react";
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuPortal,
} from "@calcom/ui/Dropdown";
import { Icon } from "@calcom/ui/Icon";
import Button from "@calcom/ui/v2/core/Button";
@@ -43,22 +48,24 @@ const DropdownActions = ({
) : (
<DropdownMenuTrigger asChild>{actionTrigger}</DropdownMenuTrigger>
)}
<DropdownMenuContent portalled>
{actions.map((action) => (
<DropdownMenuItem key={action.id} className="focus-visible:outline-none">
<Button
type="button"
color="minimal"
className="w-full rounded-none font-normal"
href={action.href}
StartIcon={action.icon}
onClick={action.onClick || defaultAction}
data-testid={action.id}>
{action.label}
</Button>
</DropdownMenuItem>
))}
</DropdownMenuContent>
<DropdownMenuPortal>
<DropdownMenuContent>
{actions.map((action) => (
<DropdownMenuItem key={action.id} className="focus-visible:outline-none">
<Button
type="button"
color="minimal"
className="w-full rounded-none font-normal"
href={action.href}
StartIcon={action.icon}
onClick={action.onClick || defaultAction}
data-testid={action.id}>
{action.label}
</Button>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenuPortal>
</Dropdown>
);
};
@@ -70,6 +77,7 @@ const TableActions: FC<Props> = ({ actions }) => {
{actions.map((action) => {
const button = (
<Button
className="whitespace-nowrap"
key={action.id}
data-testid={action.id}
href={action.href}
@@ -91,11 +91,10 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
const isCurrentUsernamePremium =
user && user.metadata && hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false;
const [isInputUsernamePremium, setIsInputUsernamePremium] = useState(false);
const debouncedApiCall = useCallback(
debounce(async (username) => {
debounce(async (username: string) => {
const { data } = await fetchUsername(username);
setMarkAsError(!data.available && username && username !== currentUsername);
setMarkAsError(!data.available && !!currentUsername && username !== currentUsername);
setIsInputUsernamePremium(data.premium);
setUsernameIsAvailable(data.available);
}, 150),
@@ -1,3 +1,4 @@
import classNames from "classnames";
import React from "react";
import { SkeletonText } from "@calcom/ui/v2";
@@ -32,9 +33,13 @@ function SkeletonItem() {
);
}
export const AvailabilitySelectSkeletonLoader = () => {
export const SelectSkeletonLoader = ({ className }: { className?: string }) => {
return (
<li className="group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3">
<li
className={classNames(
"group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3",
className
)}>
<div className="flex-grow truncate text-sm">
<div className="flex justify-between">
<SkeletonText className="h-4 w-32" />
@@ -1,5 +1,6 @@
import { FormValues } from "pages/v2/event-types/[type]";
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,15 +8,45 @@ 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 Select from "@calcom/ui/v2/core/form/select";
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
import { AvailabilitySelectSkeletonLoader } from "@components/v2/availability/SkeletonLoader";
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 = ({
@@ -30,7 +61,7 @@ const AvailabilitySelect = ({
}) => {
const { data, isLoading } = trpc.useQuery(["viewer.availability.list"]);
if (isLoading) {
return <AvailabilitySelectSkeletonLoader />;
return <SelectSkeletonLoader />;
}
const schedules = data?.schedules || [];
@@ -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/v2/event-types/[type]";
import { useEffect, useRef, useState } from "react";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
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";
@@ -40,9 +42,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
const { t } = useLocale();
const [showEventNameTip, setShowEventNameTip] = useState(false);
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 [seatsPerTimeSlot, setSeatsPerTimeSlot] = useState(eventType.seatsPerTimeSlot || 2);
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
eventType.customInputs.sort((a, b) => a.id - b.id) || []
);
@@ -50,12 +51,7 @@ 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 = !!eventType.seatsPerTimeSlot;
useEffect(() => {
animationRef.current && autoAnimate(animationRef.current);
}, [animationRef]);
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
const removeCustom = (index: number) => {
formMethods.getValues("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}
/>
)}
/>
@@ -229,129 +196,158 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
name="hideCalendarNotes"
control={formMethods.control}
defaultValue={eventType.hideCalendarNotes}
render={({ field: { value, onChange } }) => (
<SettingsToggle
title={t("disable_notes")}
description={t("disable_notes_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
)}
/>
<hr />
<Controller
name="metadata.additionalNotesRequired"
control={formMethods.control}
defaultValue={!!eventType.metadata.additionalNotesRequired}
render={({ field: { value, onChange } }) => (
<div className="flex space-x-3 ">
<Switch
name="hideCalendarNotes"
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("disable_notes")}
</Skeleton>
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
{t("disable_notes_description")}
</Skeleton>
</div>
</div>
)}
/>
<hr />
<Controller
name="hashedLink"
name="successRedirectUrl"
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="">
<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
disabled
name="hashedLink"
label={t("private_link_label")}
data-testid="generated-hash-url"
label={t("redirect_success_booking")}
labelSrOnly
placeholder={t("external_redirect_url")}
required={redirectUrlVisible}
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>
}
defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")}
/>
</div>
)}
</SettingsToggle>
</>
)}
/>
<hr />
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<>
<div className="flex space-x-3">
<Switch
name="seatsPerTimeSlot"
checked={seatsInputVisible}
onCheckedChange={(e) => {
setSeatsInputVisible(e);
formMethods.setValue("seatsPerTimeSlot", seatsPerTimeSlot);
onChange(e ? seatsPerTimeSlot : null);
}}
fitToHeight={true}
/>
<div className="flex flex-col">
<Label className="text-sm font-semibold leading-none text-black">{t("offer_seats")}</Label>
<p className="-mt-2 text-sm leading-normal text-gray-600">{t("offer_seats_description")}</p>
</div>
</div>
{seatsInputVisible && (
<div className="">
<TextField
required
name="seatsPerTimeSlot"
label={t("number_of_seats")}
type="number"
defaultValue={seatsPerTimeSlot || 2}
addOnSuffix={<>{t("seats")}</>}
onChange={(e) => {
formMethods.setValue("seatsPerTimeSlot", Number(e.target.value));
<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");
}
}}
/>
</div>
)}
</>
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 } }) => (
<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>
)}
/>
</SettingsToggle>
)}
/>
{showEventNameTip && (
<Dialog open={showEventNameTip} onOpenChange={setShowEventNameTip}>
<DialogContent
+120 -194
View File
@@ -1,208 +1,134 @@
import { EventTypeSetupInfered, FormValues } from "pages/v2/event-types/[type]";
import React, { useState } from "react";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import React from "react";
import { useFormContext } from "react-hook-form";
import { Controller } from "react-hook-form";
import { FormattedNumber, IntlProvider } from "react-intl";
import { SelectGifInput } from "@calcom/app-store/giphy/components";
import RainbowInstallForm from "@calcom/app-store/rainbow/components/RainbowInstallForm";
import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated";
import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
import { EventTypeAppsList } from "@calcom/app-store/utils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui";
import { Alert, Button, EmptyScreen, Select, Switch, TextField } from "@calcom/ui/v2";
import ErrorBoundary from "@calcom/ui/ErrorBoundary";
import { Button, EmptyScreen } from "@calcom/ui/v2";
const AppCard = ({
logo,
name,
description,
switchOnClick,
switchChecked,
children,
}: {
logo: string;
name: string;
description: React.ReactNode;
switchChecked?: boolean;
switchOnClick?: (e: boolean) => void;
children?: React.ReactNode;
}) => {
return (
<div className="mb-4 rounded-md border border-gray-200 p-8">
<div className="flex w-full">
<img src={logo} alt={name} className="mr-3 h-auto w-[42px] rounded-sm" />
<div className="flex flex-col">
<span className="font-semibold leading-none text-black">{name}</span>
<p className="pt-2 text-sm font-normal text-gray-600">{description}</p>
</div>
<div className="ml-auto flex items-center">
<Switch onCheckedChange={switchOnClick} checked={switchChecked} />
</div>
</div>
{children}
</div>
);
};
type EventType = Pick<EventTypeSetupInfered, "eventType">["eventType"] &
EventTypeAppCardComponentProps["eventType"];
export const EventAppsTab = ({
hasPaymentIntegration,
currency,
function AppCardWrapper({
app,
eventType,
hasGiphyIntegration,
hasRainbowIntegration,
}: Pick<
EventTypeSetupInfered,
"eventType" | "hasPaymentIntegration" | "hasGiphyIntegration" | "hasRainbowIntegration" | "currency"
>) => {
const formMethods = useFormContext<FormValues>();
const [showGifSelection, setShowGifSelection] = useState(
hasGiphyIntegration && !!eventType.metadata["giphyThankYouPage"]
);
const [showRainbowSection, setShowRainbowSection] = useState(
hasRainbowIntegration &&
!!eventType.metadata["blockchainId"] &&
!!eventType.metadata["smartContractAddress"]
);
const [requirePayment, setRequirePayment] = useState(eventType.price > 0);
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
getAppData,
setAppData,
}: {
app: inferQueryOutput<"viewer.apps">[number];
eventType: EventType;
getAppData: GetAppData;
setAppData: SetAppData;
}) {
const dirName = app.slug === "stripe" ? "stripepayment" : app.slug;
const Component = EventTypeAddonMap[dirName as keyof typeof EventTypeAddonMap];
const getCurrencySymbol = (locale: string, currency: string) =>
(0)
.toLocaleString(locale, {
style: "currency",
currency,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})
.replace(/\d/g, "")
.trim();
const { t } = useLocale();
const installedApps = [hasPaymentIntegration, hasGiphyIntegration].filter(Boolean).length;
if (installedApps === 0) {
return (
<div className="pt-4 before:border-0">
<EmptyScreen
Icon={Icon.FiGrid}
headline={t("empty_installed_apps_headline")}
description={t("empty_installed_apps_description")}
buttonRaw={
<Button target="_blank" color="secondary" href="/apps">
{t("empty_installed_apps_button")}{" "}
</Button>
}
/>
</div>
);
if (!Component) {
throw new Error('No component found for "' + dirName + '"');
}
return (
<div className="pt-4 before:border-0">
{/* TODO:Strip isnt fully setup yet */}
{hasPaymentIntegration && (
<AppCard
name="Stripe"
switchChecked={requirePayment}
switchOnClick={(e) => {
if (!e) {
formMethods.setValue("price", 0);
setRequirePayment(false);
} else {
setRequirePayment(true);
}
}}
description={
<>
<div className="">
{t("require_payment")} (0.5% +{" "}
<IntlProvider locale="en">
<FormattedNumber value={0.1} style="currency" currency={currency} />
</IntlProvider>{" "}
{t("commission_per_transaction")})
</div>
</>
}
logo="/api/app-store/stripepayment/icon.svg">
<>
{recurringEventDefined ? (
<Alert severity="warning" title={t("warning_recurring_event_payment")} />
) : (
requirePayment && (
<div className="block items-center sm:flex">
<Controller
defaultValue={eventType.price}
control={formMethods.control}
name="price"
render={({ field }) => (
<TextField
label=""
addOnLeading={<>{getCurrencySymbol("en", currency)}</>}
{...field}
step="0.01"
min="0.5"
type="number"
required
className="block w-full rounded-sm border-gray-300 pl-2 pr-12 text-sm"
placeholder="Price"
onChange={(e) => {
field.onChange(e.target.valueAsNumber * 100);
}}
value={field.value > 0 ? field.value / 100 : undefined}
/>
)}
/>
</div>
)
)}
</>
</AppCard>
)}
{hasGiphyIntegration && (
<AppCard
name="Giphy"
description={t("confirmation_page_gif")}
logo="/api/app-store/giphy/icon.svg"
switchOnClick={(e) => {
if (!e) {
setShowGifSelection(false);
formMethods.setValue("giphyThankYouPage", "");
} else {
setShowGifSelection(true);
}
}}
switchChecked={showGifSelection}>
{showGifSelection && (
<SelectGifInput
defaultValue={eventType.metadata["giphyThankYouPage"] as string}
onChange={(url: string) => {
formMethods.setValue("giphyThankYouPage", url);
}}
/>
)}
</AppCard>
)}
{hasRainbowIntegration && (
<AppCard
name="Rainbow"
description={t("confirmation_page_rainbow")}
logo="/api/app-store/rainbow/icon.svg"
switchOnClick={(e) => {
if (!e) {
formMethods.setValue("blockchainId", 1);
formMethods.setValue("smartContractAddress", "");
}
<ErrorBoundary message={`There is some problem with ${app.name} App`}>
<EventTypeAppContext.Provider value={[getAppData, setAppData]}>
<Component key={app.slug} app={app} eventType={eventType} />
</EventTypeAppContext.Provider>
</ErrorBoundary>
);
}
setShowRainbowSection(e);
}}
switchChecked={showRainbowSection}>
{showRainbowSection && (
<RainbowInstallForm
formMethods={formMethods}
blockchainId={(eventType.metadata.blockchainId as number) || 1}
smartContractAddress={(eventType.metadata.smartContractAddress as string) || ""}
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
const { t } = useLocale();
const { data: eventTypeApps, isLoading } = trpc.useQuery([
"viewer.apps",
{
extendsFeature: "EventType",
},
]);
const methods = useFormContext<FormValues>();
const installedApps = eventTypeApps?.filter((app) => app.credentials.length);
const notInstalledApps = eventTypeApps?.filter((app) => !app.credentials.length);
const allAppsData = methods.watch("metadata")?.apps || {};
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
methods.setValue("metadata", {
...methods.getValues("metadata"),
apps: _allAppsData,
});
};
const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => {
return function (key) {
const appData = allAppsData[appId as keyof typeof allAppsData] || {};
if (key) {
return appData[key as keyof typeof appData];
}
return appData;
};
};
const getAppDataSetter = (appId: EventTypeAppsList): SetAppData => {
return function (key, value) {
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
const appData = allAppsDataFromForm[appId];
setAllAppsData({
...allAppsDataFromForm,
[appId]: {
...appData,
[key]: value,
},
});
};
};
return (
<>
<div>
<div className="before:border-0">
{!isLoading && !installedApps?.length ? (
<EmptyScreen
Icon={Icon.FiGrid}
headline={t("empty_installed_apps_headline")}
description={t("empty_installed_apps_description")}
buttonRaw={
<Button target="_blank" color="secondary" href="/apps">
{t("empty_installed_apps_button")}{" "}
</Button>
}
/>
)}
</AppCard>
)}
</div>
) : null}
{installedApps?.map((app) => (
<AppCardWrapper
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug}
app={app}
eventType={eventType}
/>
))}
</div>
</div>
<div>
{!isLoading && notInstalledApps?.length ? (
<h2 className="mt-0 mb-2 text-lg font-semibold text-gray-900">Available Apps</h2>
) : null}
<div className="before:border-0">
{notInstalledApps?.map((app) => (
<AppCardWrapper
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug}
app={app}
eventType={eventType}
/>
))}
</div>
</div>
</>
);
};
@@ -1,19 +1,21 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group";
import { EventTypeSetupInfered, FormValues } from "pages/v2/event-types/[type]";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { useFormContext, Controller, useWatch } from "react-hook-form";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { PeriodType } from "@calcom/prisma/client";
import { Select, Switch, Label, Input, MinutesField } from "@calcom/ui/v2";
import type { BookingLimit } from "@calcom/types/Calendar";
import { Icon } from "@calcom/ui";
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">) => {
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const { eventType } = props;
const PERIOD_TYPES = [
{
type: "ROLLING" as const,
@@ -154,110 +156,233 @@ export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">)
/>
</div>
</div>
<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}
<hr className="my-8" />
<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 />
</SettingsToggle>
)}
/>
<hr className="my-8" />
<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>
);
};
const validationOrderKeys = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"];
type BookingLimitsKey = keyof BookingLimit;
const BookingLimits = () => {
const { watch, setValue, control } = useFormContext<FormValues>();
const watchBookingLimits = watch("bookingLimits");
const { t } = useLocale();
const [animateRef] = useAutoAnimate<HTMLUListElement>();
const BOOKING_LIMIT_OPTIONS: {
value: keyof BookingLimit;
label: string;
}[] = [
{
value: "PER_DAY",
label: "Per Day",
},
{
value: "PER_WEEK",
label: "Per Week",
},
{
value: "PER_MONTH",
label: "Per Month",
},
{
value: "PER_YEAR",
label: "Per Year",
},
];
return (
<Controller
name="bookingLimits"
control={control}
render={({ field: { value, onChange } }) => {
const currentBookingLimits = value;
return (
<ul ref={animateRef}>
{currentBookingLimits &&
watchBookingLimits &&
Object.entries(currentBookingLimits)
.sort(([limitkeyA], [limitKeyB]) => {
return (
validationOrderKeys.indexOf(limitkeyA as BookingLimitsKey) -
validationOrderKeys.indexOf(limitKeyB as BookingLimitsKey)
);
})
.map(([key, bookingAmount]) => {
const bookingLimitKey = key as BookingLimitsKey;
return (
<div className="mb-2 flex items-center space-x-2 text-sm" key={bookingLimitKey}>
<Input
id={`${bookingLimitKey}-limit`}
type="number"
className="mb-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
placeholder="1"
min={1}
defaultValue={bookingAmount}
onChange={(e) => {
const val = e.target.value;
setValue(`bookingLimits.${bookingLimitKey}`, parseInt(val));
}}
/>
<Select
options={BOOKING_LIMIT_OPTIONS.filter(
(option) => !Object.keys(currentBookingLimits).includes(option.value)
)}
isSearchable={false}
defaultValue={BOOKING_LIMIT_OPTIONS.find((option) => option.value === key)}
onChange={(val) => {
const current = currentBookingLimits;
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
delete current[bookingLimitKey];
const newData = {
...current,
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
[val?.value as BookingLimitsKey]: watchBookingLimits[bookingLimitKey],
};
onChange(newData);
}}
/>
<Button
size="icon"
StartIcon={Icon.FiTrash}
color="destructive"
onClick={() => {
const current = currentBookingLimits;
delete current[key as BookingLimitsKey];
onChange(current);
}}
/>
</div>
);
})}
{currentBookingLimits && Object.keys(currentBookingLimits).length <= 3 && (
<Button
color="minimal"
StartIcon={Icon.FiPlus}
onClick={() => {
if (!currentBookingLimits || !watchBookingLimits) return;
const currentKeys = Object.keys(watchBookingLimits);
const rest = Object.values(BOOKING_LIMIT_OPTIONS).filter(
(option) => !currentKeys.includes(option.value)
);
if (!rest || !currentKeys) return;
//currentBookingLimits is always defined so can be casted
setValue("bookingLimits", {
...watchBookingLimits,
[rest[0].value]: undefined,
});
}}>
{t("add_limit")}
</Button>
)}
</ul>
);
}}
/>
);
};
@@ -1,25 +1,18 @@
import { EventTypeSetupInfered, FormValues } from "pages/v2/event-types/[type]";
import { EventTypeSetupInfered } from "pages/event-types/[type]";
import { useState } from "react";
import { useFormContext } from "react-hook-form";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import RecurringEventController from "./RecurringEventController";
export const EventRecurringTab = ({
eventType,
hasPaymentIntegration,
}: Pick<EventTypeSetupInfered, "eventType" | "hasPaymentIntegration">) => {
const requirePayment = eventType.price > 0;
const [recurringEventDefined, setRecurringEventDefined] = useState(
eventType.recurringEvent?.count !== undefined
);
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupInfered, "eventType">) => {
const stripeAppData = getStripeAppData(eventType);
const requirePayment = stripeAppData.price > 0;
return (
<div className="">
<RecurringEventController
paymentEnabled={hasPaymentIntegration && requirePayment}
onRecurringEventDefined={setRecurringEventDefined}
recurringEvent={eventType.recurringEvent}
/>
<RecurringEventController paymentEnabled={requirePayment} recurringEvent={eventType.recurringEvent} />
</div>
);
};
@@ -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/v2/event-types/[type]";
import { useEffect, useRef, useState } from "react";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
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>
@@ -1,5 +1,5 @@
import { SchedulingType } from "@prisma/client/";
import { EventTypeSetupInfered, FormValues } from "pages/v2/event-types/[type]";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useMemo } from "react";
import { Controller, useFormContext } from "react-hook-form";
@@ -1,6 +1,6 @@
import { TFunction } from "next-i18next";
import { useRouter } from "next/router";
import { EventTypeSetupInfered, FormValues } from "pages/v2/event-types/[type]";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useMemo, useState } from "react";
import { Loader } from "react-feather";
import { UseFormReturn } from "react-hook-form";
@@ -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,
@@ -42,6 +43,7 @@ type Props = {
team: EventTypeSetupInfered["team"];
disableBorder?: boolean;
enabledAppsNumber: number;
installedAppsNumber: number;
enabledWorkflowsNumber: number;
formMethods: UseFormReturn<FormValues>;
};
@@ -51,8 +53,9 @@ function getNavigation(props: {
eventType: Props["eventType"];
enabledAppsNumber: number;
enabledWorkflowsNumber: number;
installedAppsNumber: number;
}) {
const { eventType, t, enabledAppsNumber, enabledWorkflowsNumber } = props;
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
return [
{
name: "event_setup_tab_title",
@@ -88,7 +91,8 @@ function getNavigation(props: {
name: "apps",
href: `/event-types/${eventType.id}?tabName=apps`,
icon: Icon.FiGrid,
info: `${enabledAppsNumber} ${t("active")}`,
//TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
},
{
name: "workflows",
@@ -106,6 +110,7 @@ function EventTypeSingleLayout({
team,
disableBorder,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
formMethods,
}: Props) {
@@ -136,7 +141,13 @@ function EventTypeSingleLayout({
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
const navigation = getNavigation({ t, eventType, enabledAppsNumber, enabledWorkflowsNumber });
const navigation = getNavigation({
t,
eventType,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
});
// If there is a team put this navigation item within the tabs
if (team)
navigation.splice(2, 0, {
@@ -146,7 +157,7 @@ function EventTypeSingleLayout({
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
});
return navigation;
}, [t, eventType, enabledAppsNumber, enabledWorkflowsNumber, team]);
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
eventType.slug
@@ -157,16 +168,16 @@ function EventTypeSingleLayout({
return (
<Shell
backPath="/event-types"
title={t("event_type_title", { eventTypeTitle: eventType.title })}
title={eventType.title + " | " + t("event_type")}
heading={eventType.title}
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
@@ -221,37 +232,55 @@ 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>
<DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100">
<Button
color="minimal"
StartIcon={Icon.FiExternalLink}
target="_blank"
href={permalink}
rel="noreferrer">
rel="noreferrer"
className="min-w-full">
{t("preview")}
</Button>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100">
<Button color="minimal" StartIcon={Icon.FiLink}>
{t("copy_link")}
</Button>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100">
<Button
color="minimal"
StartIcon={Icon.FiTrash}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}>
onClick={() => setDeleteDialogOpen(true)}
className="min-w-full">
{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,18 +6,16 @@ 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;
paymentEnabled: boolean;
onRecurringEventDefined: (value: boolean) => void;
};
export default function RecurringEventController({
recurringEvent,
paymentEnabled,
onRecurringEventDefined,
}: RecurringEventControllerProps) {
const { t } = useLocale();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(recurringEvent);
@@ -38,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>
@@ -1,4 +1,4 @@
import { useMutation } from "react-query";
import { useMutation } from "@tanstack/react-query";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
@@ -1,245 +0,0 @@
import { Webhook } from "@prisma/client";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import customTemplate, { hasTemplateIntegration } from "@calcom/features/webhooks/utils/integrationTemplate";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { DialogFooter } from "@calcom/ui/Dialog";
import Switch from "@calcom/ui/Switch";
import { FieldsetLegend, Form, InputGroupBox, TextArea, TextField } from "@calcom/ui/form/fields";
import { WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP } from "@lib/webhooks/constants";
import { TWebhook } from "@components/webhook/WebhookListItem";
import WebhookTestDisclosure from "@components/webhook/WebhookTestDisclosure";
/** @deprecated Moved to `packages/features/webhooks` */
export default function WebhookDialogForm(props: {
eventTypeId?: number;
defaultValues?: TWebhook;
app?: string;
handleClose: () => void;
webhooks: Webhook[];
}) {
const { t } = useLocale();
const utils = trpc.useContext();
const appId = props.app;
const webhooks = props.webhooks;
const triggers = !appId
? WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP["core"]
: WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP[appId as keyof typeof WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP];
const {
defaultValues = {
id: "",
eventTriggers: triggers,
subscriberUrl: "",
active: true,
payloadTemplate: null,
secret: null,
} as Omit<TWebhook, "userId" | "createdAt" | "eventTypeId" | "appId">,
} = props;
const [useCustomPayloadTemplate, setUseCustomPayloadTemplate] = useState(!!defaultValues.payloadTemplate);
const [changeSecret, setChangeSecret] = useState(false);
const [newSecret, setNewSecret] = useState("");
const hasSecretKey = !!defaultValues.secret;
const currentSecret = defaultValues.secret;
const subscriberUrlReserved = (subscriberUrl: string, id: string): boolean => {
return !!webhooks.find((webhook) => webhook.subscriberUrl === subscriberUrl && webhook.id !== id);
};
const form = useForm({
defaultValues,
});
const handleInput = (event: React.FormEvent<HTMLInputElement>) => {
setNewSecret(event.currentTarget.value);
};
useEffect(() => {
if (changeSecret) {
form.unregister("secret", { keepDefaultValue: false });
}
}, [changeSecret]);
return (
<Form
data-testid="WebhookDialogForm"
form={form}
handleSubmit={async (event) => {
if (subscriberUrlReserved(event.subscriberUrl, event.id)) {
showToast(t("webhook_subscriber_url_reserved"), "error");
return;
}
const e = changeSecret
? { ...event, eventTypeId: props.eventTypeId, appId }
: { ...event, secret: currentSecret, eventTypeId: props.eventTypeId, appId };
if (!useCustomPayloadTemplate && event.payloadTemplate) {
event.payloadTemplate = null;
}
if (event.id) {
await utils.client.mutation("viewer.webhook.edit", e);
await utils.invalidateQueries(["viewer.webhook.list"]);
showToast(t("webhook_updated_successfully"), "success");
} else {
await utils.client.mutation("viewer.webhook.create", e);
await utils.invalidateQueries(["viewer.webhook.list"]);
showToast(t("webhook_created_successfully"), "success");
}
props.handleClose();
}}
className="space-y-4">
<div>
<input type="hidden" {...form.register("id")} />
</div>
<fieldset className="space-y-2">
<InputGroupBox className="border-0 bg-gray-50">
<Controller
control={form.control}
name="active"
render={({ field }) => (
<Switch
label={field.value ? t("webhook_enabled") : t("webhook_disabled")}
defaultChecked={field.value}
onCheckedChange={(isChecked) => {
form.setValue("active", isChecked);
}}
/>
)}
/>
</InputGroupBox>
</fieldset>
<div>
<TextField
label={t("subscriber_url")}
{...form.register("subscriberUrl")}
required
type="url"
onChange={(e) => {
form.setValue("subscriberUrl", e.target.value);
if (hasTemplateIntegration({ url: e.target.value })) {
setUseCustomPayloadTemplate(true);
form.setValue("payloadTemplate", customTemplate({ url: e.target.value }));
}
}}
/>
</div>
<fieldset className="space-y-2">
<FieldsetLegend>{t("event_triggers")}</FieldsetLegend>
<InputGroupBox className="border-0 bg-gray-50">
{triggers.map((key) => (
<Controller
key={key}
control={form.control}
name="eventTriggers"
render={({ field }) => (
<Switch
label={t(key.toLowerCase())}
defaultChecked={field.value.includes(key)}
onCheckedChange={(isChecked) => {
const value = field.value;
const newValue = isChecked ? [...value, key] : value.filter((v) => v !== key);
form.setValue("eventTriggers", newValue, {
shouldDirty: true,
});
}}
/>
)}
/>
))}
</InputGroupBox>
</fieldset>
<fieldset className="space-y-2">
{!!hasSecretKey && !changeSecret && (
<>
<FieldsetLegend>{t("secret")}</FieldsetLegend>
<div className="rounded-sm bg-gray-50 p-2 text-xs text-neutral-900">
{t("forgotten_secret_description")}
</div>
<Button
color="secondary"
type="button"
className="py-1 text-xs"
onClick={() => {
setChangeSecret(true);
}}>
{t("change_secret")}
</Button>
</>
)}
{!!hasSecretKey && changeSecret && (
<>
<TextField
autoComplete="off"
label={t("secret")}
{...form.register("secret")}
value={newSecret}
onChange={handleInput}
type="text"
placeholder={t("leave_blank_to_remove_secret")}
/>
<Button
color="secondary"
type="button"
className="py-1 text-xs"
onClick={() => {
setChangeSecret(false);
}}>
{t("cancel")}
</Button>
</>
)}
{!hasSecretKey && (
<TextField autoComplete="off" label={t("secret")} {...form.register("secret")} type="text" />
)}
</fieldset>
<fieldset className="space-y-2">
<FieldsetLegend>{t("payload_template")}</FieldsetLegend>
<div className="space-x-3 text-sm rtl:space-x-reverse">
<label>
<input
className="text-neutral-900 focus:ring-neutral-500"
type="radio"
name="useCustomPayloadTemplate"
onChange={(value) => setUseCustomPayloadTemplate(!value.target.checked)}
defaultChecked={!useCustomPayloadTemplate}
/>{" "}
Default
</label>
<label>
<input
className="text-neutral-900 focus:ring-neutral-500"
onChange={(value) => setUseCustomPayloadTemplate(value.target.checked)}
name="useCustomPayloadTemplate"
type="radio"
defaultChecked={useCustomPayloadTemplate}
/>{" "}
Custom
</label>
</div>
{useCustomPayloadTemplate && (
<TextArea
{...form.register("payloadTemplate")}
defaultValue={useCustomPayloadTemplate && (defaultValues.payloadTemplate || "")}
rows={3}
/>
)}
</fieldset>
<WebhookTestDisclosure />
<DialogFooter>
<Button type="button" color="secondary" onClick={props.handleClose} tabIndex={-1}>
{t("cancel")}
</Button>
<Button type="submit" loading={form.formState.isSubmitting}>
{t("save")}
</Button>
</DialogFooter>
</Form>
);
}
@@ -1,103 +0,0 @@
import { useRouter } from "next/router";
import { useState } from "react";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
import { List } from "@calcom/ui/List";
import { ShellSubHeading } from "@calcom/ui/Shell";
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
import { QueryCell } from "@lib/QueryCell";
import WebhookDialogForm from "@components/webhook/WebhookDialogForm";
import WebhookListItem, { TWebhook } from "@components/webhook/WebhookListItem";
export type WebhookListContainerType = {
title: string;
subtitle: string;
eventTypeId?: number;
appId?: string;
};
/** @deprecated Moved to `packages/features/webhooks` */
export default function WebhookListContainer(props: WebhookListContainerType) {
const router = useRouter();
const query = trpc.useQuery(
["viewer.webhook.list", { eventTypeId: props.eventTypeId, appId: props.appId }],
{
suspense: true,
enabled: router.isReady,
}
);
const [newWebhookModal, setNewWebhookModal] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [editing, setEditing] = useState<TWebhook | null>(null);
return (
<QueryCell
query={query}
customLoader={<SkeletonLoader />}
success={({ data }) => (
<div className="border-b border-gray-200 py-8 pl-2 pr-1">
<ShellSubHeading
className="mt-2"
title={props.title}
subtitle={props.subtitle}
actions={
<Button
color="minimal"
size="icon"
StartIcon={Icon.FiPlus}
onClick={() => setNewWebhookModal(true)}
data-testid="new_webhook"
/>
}
/>
{data.length ? (
<List className="mt-6">
{data.map((item) => (
<WebhookListItem
key={item.id}
webhook={item}
onEditWebhook={() => {
setEditing(item);
setEditModalOpen(true);
}}
/>
))}
</List>
) : null}
{/* New webhook dialog */}
<Dialog open={newWebhookModal} onOpenChange={(isOpen) => !isOpen && setNewWebhookModal(false)}>
<DialogContent>
<WebhookDialogForm
app={props.appId}
webhooks={data}
eventTypeId={props.eventTypeId}
handleClose={() => setNewWebhookModal(false)}
/>
</DialogContent>
</Dialog>
{/* Edit webhook dialog */}
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
<DialogContent>
{editing && (
<WebhookDialogForm
app={props.appId}
key={editing.id}
webhooks={data}
eventTypeId={props.eventTypeId || undefined}
handleClose={() => setEditModalOpen(false)}
defaultValues={editing}
/>
)}
</DialogContent>
</Dialog>
</div>
)}
/>
);
}
@@ -1,94 +0,0 @@
import classNames from "@calcom/lib/classNames";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
import { ListItem } from "@calcom/ui/List";
import { Tooltip } from "@calcom/ui/Tooltip";
import { useLocale } from "@lib/hooks/useLocale";
export type TWebhook = inferQueryOutput<"viewer.webhook.list">[number];
/** @deprecated Moved to `packages/features/webhooks` */
export default function WebhookListItem(props: { webhook: TWebhook; onEditWebhook: () => void }) {
const { t } = useLocale();
const utils = trpc.useContext();
const deleteWebhook = trpc.useMutation("viewer.webhook.delete", {
async onSuccess() {
await utils.invalidateQueries(["viewer.webhook.list"]);
},
});
return (
<ListItem className={classNames("flex w-full p-4", props.webhook.active ? "bg-white" : "bg-gray-100")}>
<div className="flex w-full justify-between">
<div className="flex max-w-full flex-col truncate">
<div className="flex space-y-1">
<span
className={classNames(
"truncate text-sm",
props.webhook.active ? "text-neutral-700" : "text-neutral-400"
)}>
{props.webhook.subscriberUrl}
</span>
</div>
<div className="mt-2 flex">
<span className="flex flex-col space-x-2 space-y-1 text-xs sm:flex-row sm:space-y-0 sm:rtl:space-x-reverse">
{props.webhook.eventTriggers.map((eventTrigger, ind) => (
<span
key={ind}
className={classNames(
"w-max rounded-sm px-1 text-xs",
props.webhook.active ? "text-grey-200 bg-gray-200" : "bg-grey-50 text-neutral-400"
)}>
{t(`${eventTrigger.toLowerCase()}`)}
</span>
))}
</span>
</div>
</div>
<div className="flex">
<Tooltip side="top" content={t("edit_webhook")}>
<Button
onClick={() => props.onEditWebhook()}
color="minimal"
size="icon"
StartIcon={Icon.FiEdit2}
className="ml-4 w-full self-center p-2"
/>
</Tooltip>
<Dialog>
<Tooltip side="top" content={t("delete_webhook")}>
<DialogTrigger asChild>
<Button
onClick={(e) => {
e.stopPropagation();
}}
size="icon"
color="warn"
StartIcon={Icon.FiTrash}
className="ml-2 w-full self-center p-2"
/>
</DialogTrigger>
</Tooltip>
<ConfirmationDialogContent
variety="danger"
title={t("delete_webhook")}
confirmBtnText={t("confirm_delete_webhook")}
cancelBtnText={t("cancel")}
onConfirm={() =>
deleteWebhook.mutate({
id: props.webhook.id,
eventTypeId: props.webhook.eventTypeId || undefined,
})
}>
{t("delete_webhook_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
</div>
</div>
</ListItem>
);
}
@@ -1,63 +0,0 @@
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import { useState } from "react";
import { useWatch } from "react-hook-form";
import classNames from "@calcom/lib/classNames";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Icon } from "@calcom/ui/Icon";
import { InputGroupBox } from "@calcom/ui/form/fields";
import { useLocale } from "@lib/hooks/useLocale";
/** @deprecated Moved to `packages/features/webhooks` */
export default function WebhookTestDisclosure() {
const subscriberUrl: string = useWatch({ name: "subscriberUrl" });
const payloadTemplate = useWatch({ name: "payloadTemplate" }) || null;
const { t } = useLocale();
const [open, setOpen] = useState(false);
const mutation = trpc.useMutation("viewer.webhook.testTrigger", {
onError(err) {
showToast(err.message, "error");
},
});
return (
<Collapsible open={open} onOpenChange={() => setOpen(!open)}>
<CollapsibleTrigger type="button" className="flex w-full cursor-pointer">
<Icon.FiChevronRight className={`${open ? "rotate-90 transform" : ""} h-5 w-5 text-neutral-500`} />
<span className="text-sm font-medium text-gray-700">{t("webhook_test")}</span>
</CollapsibleTrigger>
<CollapsibleContent>
<InputGroupBox className="space-y-0 border-0 px-0">
<div className="flex justify-between bg-gray-50 p-2">
<h3 className="self-center text-gray-700">{t("webhook_response")}</h3>
<Button
type="button"
color="minimal"
disabled={mutation.isLoading}
onClick={() => mutation.mutate({ url: subscriberUrl, type: "PING", payloadTemplate })}>
{t("ping_test")}
</Button>
</div>
<div className="border-8 border-gray-50 p-2 text-gray-500">
{!mutation.data && <em>{t("no_data_yet")}</em>}
{mutation.status === "success" && (
<>
<div
className={classNames(
"ml-auto w-max px-2 py-1 text-xs",
mutation.data.ok ? "bg-green-50 text-green-500" : "bg-red-50 text-red-500"
)}>
{mutation.data.ok ? t("success") : t("failed")}
</div>
<pre className="overflow-x-auto">{JSON.stringify(mutation.data, null, 4)}</pre>
</>
)}
</div>
</InputGroupBox>
</CollapsibleContent>
</Collapsible>
);
}
+5 -9
View File
@@ -1,16 +1,14 @@
import { ReactNode } from "react";
import {
QueryObserverIdleResult,
QueryObserverLoadingErrorResult,
QueryObserverLoadingResult,
QueryObserverRefetchErrorResult,
QueryObserverSuccessResult,
UseQueryResult,
} from "react-query";
} from "@tanstack/react-query";
import { ReactNode } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import type { UseTRPCQueryOptions } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type {
inferHandlerInput,
@@ -22,6 +20,8 @@ import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Alert } from "@calcom/ui/Alert";
import Loader from "@calcom/ui/Loader";
import type { UseTRPCQueryOptions } from "@trpc/react/shared";
type ErrorLike = {
message: string;
};
@@ -34,7 +34,6 @@ interface QueryCellOptionsBase<TData, TError extends ErrorLike> {
query: QueryObserverLoadingErrorResult<TData, TError> | QueryObserverRefetchErrorResult<TData, TError>
) => JSXElementOrNull;
loading?: (query: QueryObserverLoadingResult<TData, TError> | null) => JSXElementOrNull;
idle?: (query: QueryObserverIdleResult<TData, TError>) => JSXElementOrNull;
}
interface QueryCellOptionsNoEmpty<TData, TError extends ErrorLike>
@@ -84,14 +83,11 @@ export function QueryCell<TData, TError extends ErrorLike>(
);
}
if (query.status === "idle") {
return opts.idle?.(query) ?? StatusLoader;
}
// impossible state
return null;
}
type inferProcedures<TObj extends ProcedureRecord<any, any, any, any, any, any>> = {
type inferProcedures<TObj extends ProcedureRecord> = {
[TPath in keyof TObj]: {
input: inferProcedureInput<TObj[TPath]>;
output: inferProcedureOutput<TObj[TPath]>;
+4 -3
View File
@@ -9,7 +9,7 @@ import { ComponentProps, ReactNode } from "react";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { trpc } from "@calcom/trpc/react";
import { trpc, proxy } from "@calcom/trpc/react";
import { MetaProvider } from "@calcom/ui/v2/core/Meta";
import usePublicPage from "@lib/hooks/usePublicPage";
@@ -38,7 +38,8 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
* We intend to not cache i18n query
**/
const { i18n, locale } = trpc.useQuery(["viewer.public.i18n"], { context: { skipBatch: true } }).data ?? {
const { i18n, locale } = trpc.useQuery(["viewer.public.i18n"], { trpc: { context: { skipBatch: true } } })
.data ?? {
locale: "en",
};
@@ -54,7 +55,7 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
};
const AppProviders = (props: AppPropsWithChildren) => {
const session = trpc.useQuery(["viewer.public.session"]).data;
const session = proxy.public.session.useQuery().data;
// No need to have intercom on public pages - Good for Page Performance
const isPublicPage = usePublicPage();
const isThemeSupported =
+8 -4
View File
@@ -1,6 +1,10 @@
// handles logic related to user clock display using 24h display / timeZone options.
import dayjs from "@calcom/dayjs";
import { isBrowserLocale24h } from "@calcom/lib/timeFormat";
import {
getIs24hClockFromLocalStorage,
isBrowserLocale24h,
setIs24hClockInLocalStorage,
} from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage";
interface TimeOptions {
@@ -20,8 +24,8 @@ const initClock = () => {
return;
}
// This only sets browser locale if there's no preference on localStorage.
if (!localStorage.getItem("timeOption.is24hClock")) set24hClock(isBrowserLocale24h());
timeOptions.is24hClock = localStorage.getItem("timeOption.is24hClock") === "true";
if (getIs24hClockFromLocalStorage() === null) set24hClock(isBrowserLocale24h());
timeOptions.is24hClock = !!getIs24hClockFromLocalStorage();
timeOptions.inviteeTimeZone = localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess();
};
@@ -32,7 +36,7 @@ const is24h = (is24hClock?: boolean) => {
};
const set24hClock = (is24hClock: boolean) => {
localStorage.setItem("timeOption.is24hClock", is24hClock.toString());
setIs24hClockInLocalStorage(is24hClock);
timeOptions.is24hClock = is24hClock;
};
+1
View File
@@ -10,6 +10,7 @@ async function getBooking(prisma: PrismaClient, uid: string) {
description: true,
customInputs: true,
smsReminderNumber: true,
location: true,
attendees: {
select: {
email: true,
-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;
+37 -9
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,41 @@ export const parseRecurringDates = (
const rule = new RRule({
...restRecurringEvent,
count: recurringCount,
dtstart: dayjs(startDate).toDate(),
dtstart: dayjs(startDate).utc(true).toDate(),
});
const dateStrings = rule.all().map((r) => {
return processDate(dayjs(r).tz(timeZone), i18n);
// 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);
});
return [dateStrings, rule.all()];
return [dateStrings, times];
};
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
export const extractRecurringDates = (
bookings: BookingItem[],
booking: inferQueryOutput<"viewer.bookings">["bookings"][number] & {
eventType: { recurringEvent: RecurringEvent | null };
recurringEventId: string | null;
recurringBookings: inferQueryOutput<"viewer.bookings">["recurringInfo"];
},
timeZone: string | undefined,
i18n: I18n
): [string[], Date[]] => {
const dateStrings = bookings.map((booking) => processDate(dayjs(booking.startTime).tz(timeZone), i18n));
const allDates = dateStrings.map((dateString) => dayjs(dateString).tz(timeZone).toDate());
// 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(
(val) => val.recurringEventId === booking.recurringEventId
);
const allDates = new RRule({
...rest,
count: recurringInfo?._count.recurringEventId,
dtstart: recurringInfo?._min.startTime,
}).all();
const utcOffset = dayjs(recurringInfo?._min.startTime).tz(timeZone).utcOffset();
const dateStrings = allDates.map((r) => {
return processDate(dayjs.utc(r).utcOffset(utcOffset), i18n);
});
return [dateStrings, allDates];
};
-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[];
};
-18
View File
@@ -1,18 +0,0 @@
import { WebhookTriggerEvents } from "@prisma/client";
// this is exported as we can't use `WebhookTriggerEvents` in the frontend straight-off
export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = {
core: [
WebhookTriggerEvents.BOOKING_CANCELLED,
WebhookTriggerEvents.BOOKING_CREATED,
WebhookTriggerEvents.BOOKING_RESCHEDULED,
WebhookTriggerEvents.MEETING_ENDED,
] as const,
"routing-forms": [WebhookTriggerEvents.FORM_SUBMITTED] as ["FORM_SUBMITTED"],
};
export const WEBHOOK_TRIGGER_EVENTS = [
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.core,
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP["routing-forms"],
] as const;
-45
View File
@@ -1,45 +0,0 @@
import { WebhookTriggerEvents } from "@prisma/client";
import prisma from "@calcom/prisma";
export type GetSubscriberOptions = {
userId: number;
eventTypeId: number;
triggerEvent: WebhookTriggerEvents;
};
/** @deprecated use `packages/lib/webhooks/subscriptions.tsx` */
const getWebhooks = async (options: GetSubscriberOptions) => {
const { userId, eventTypeId } = options;
const allWebhooks = await prisma.webhook.findMany({
where: {
OR: [
{
userId,
},
{
eventTypeId,
},
],
AND: {
eventTriggers: {
has: options.triggerEvent,
},
active: {
equals: true,
},
},
},
select: {
id: true,
subscriberUrl: true,
payloadTemplate: true,
appId: true,
secret: true,
},
});
return allWebhooks;
};
export default getWebhooks;
+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 -32
View File
@@ -5,28 +5,6 @@ import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { isIpInBanlist } from "@calcom/lib/getIP";
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
const V2_WHITELIST = [
"/settings/admin",
"/settings/developer/webhooks",
"/settings/developer/api-keys",
"/settings/my-account",
"/settings/security",
"/settings/teams",
"/availability",
"/bookings",
"/event-types",
"/workflows",
"/apps",
"/teams",
"/success",
"/auth/login",
];
// For pages
// - which has V1 versions being modified as V2
// - Add routing_forms to keep old links working
const V2_BLACKLIST = ["/apps/routing_forms/", "/apps/routing-forms/", "/apps/typeform/"];
const middleware: NextMiddleware = async (req) => {
const url = req.nextUrl;
@@ -45,22 +23,19 @@ 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");
return NextResponse.rewrite(url);
}
/** Display available V2 pages */
if (
!V2_BLACKLIST.some((p) => url.pathname.startsWith(p)) &&
V2_WHITELIST.some((p) => url.pathname.startsWith(p))
) {
// rewrite to the current subdomain under the pages/sites folder
url.pathname = `/v2${url.pathname}`;
return NextResponse.rewrite(url);
}
return NextResponse.next();
};
+7 -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",
@@ -154,6 +154,11 @@ const nextConfig = {
destination: "/app-store/:path*",
permanent: true,
},
{
source: "/auth/signup",
destination: "/signup",
permanent: true,
},
{
source: "/settings",
destination: "/settings/my-account/profile",
+16 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.0.2",
"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": "*",
@@ -44,20 +44,23 @@
"@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.9.7",
"@next-auth/prisma-adapter": "^1.0.4",
"@next/bundle-analyzer": "12.1.6",
"@radix-ui/react-avatar": "^0.1.4",
"@radix-ui/react-collapsible": "^0.1.0",
"@radix-ui/react-dialog": "^0.1.0",
"@radix-ui/react-dropdown-menu": "^0.1.1",
"@radix-ui/react-id": "^0.1.0",
"@radix-ui/react-radio-group": "^0.1.1",
"@radix-ui/react-slider": "^0.1.1",
"@radix-ui/react-switch": "^0.1.1",
"@radix-ui/react-toggle-group": "^0.1.5",
"@next/bundle-analyzer": "^12.2.5",
"@radix-ui/react-avatar": "^1.0.0",
"@radix-ui/react-collapsible": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-dropdown-menu": "^1.0.0",
"@radix-ui/react-id": "^1.0.0",
"@radix-ui/react-popover": "^1.0.0",
"@radix-ui/react-radio-group": "^1.0.0",
"@radix-ui/react-slider": "^1.0.0",
"@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-toggle-group": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
"@stripe/react-stripe-js": "^1.10.0",
"@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",
@@ -105,7 +108,6 @@
"react-live-chat-loader": "^2.7.3",
"react-multi-email": "^0.5.3",
"react-phone-number-input": "^3.2.7",
"react-query": "^3.39.2",
"react-select": "^5.4.0",
"react-timezone-select": "^1.3.2",
"react-use-intercom": "1.5.1",
@@ -118,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",

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