Compare commits

..
318 Commits
Author SHA1 Message Date
Alex van Andel e819a31dd6 v3.1.9
release.yml / release (push) Canceled after 0s
2023-08-11 22:22:34 +01:00
Joe Au-YeungandOmar López 8356cb7d34 Try to list calendars, if not continue (#10720)
Co-authored-by: Omar López <zomars@me.com>
2023-08-11 22:19:26 +01:00
Alex van Andel ceb18167cf fix: Broken team events if a user with the same name exists (#10724)
* fix: Broken team events if a user with the same name exists

* Fix tests + fix usernameList optionality
2023-08-11 22:06:24 +01:00
alannncandAlex van Andel 4d5697f5a5 fix: and enable dynamic booking test (#10642)
* fix and enable dynamic booking test

* remove page pause

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-08-11 13:33:24 -07:00
Shivam Kalra afe180a0ec feat: mailhog fixture (#10606)
* feat: mailhog fixture

* fix: nodemailer to dispatch emails with e2e env

* fix: remove space from email subject

* feat: fixture getFirstEventAsOwner

* feat: assert email subjects
2023-08-11 18:56:27 +00:00
Carina WollendorferandCarinaWolli 851a388853 fix: e2e test for rescheduling overlapping time (#10721)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-08-11 11:45:53 -07:00
Crowdin Bot 07e20144df New Crowdin translations by Github Action 2023-08-11 07:05:51 +00:00
Crowdin Bot 1535e48ce5 New Crowdin translations by Github Action 2023-08-11 07:02:52 +00:00
Suyash SrivastavaandHariom Balhara 0dce391f46 feat: element call app added (#10585)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-11 06:59:20 +00:00
cfe7ea45ec test: Create unit tests for react components in packages/ui/components/form/step (#10442)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-11 12:06:55 +05:30
Crowdin Bot c244f0fc8e New Crowdin translations by Github Action 2023-08-11 06:36:30 +00:00
Zain GulbazandCarina Wollendorfer 5301af2e9f fix: If the input type "Name" is selected, the label can't be changed from our default label "Your Name" (#10618)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-11 12:03:38 +05:30
Crowdin Bot a4e7ca91b5 New Crowdin translations by Github Action 2023-08-10 21:04:24 +00:00
Leo GiovanettiandOmar López 64e6e1a19a fix: multiple duration getSchedule calls [CAL-2336] (#10709)
Co-authored-by: Omar López <zomars@me.com>
2023-08-10 21:00:46 +00:00
Carina WollendorferandCarinaWolli a6daf17909 fix: allow new booking to overlap old one when rescheduling (#10424)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-08-10 19:07:57 +00:00
Carina WollendorferandCarinaWolli c7dfa7bc89 fix: add extra security layers for sending messages to attendees (#10636)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-08-10 11:52:36 -07:00
Alex van Andel 633d85e81b v3.1.8
release.yml / release (push) Canceled after 0s
2023-08-10 19:05:29 +01:00
Leo Giovanetti 49a61207d5 fix: profile banner delay for orgs + small DX chore (#10546)
* Fixing profile banner delay for orgs

* Separating into other useEffect
2023-08-10 17:50:43 +00:00
alannnc f3eb490e3b Remove bookingIds map and conditional using in: [ ] (#10655) 2023-08-10 17:36:29 +00:00
Joe Au-Yeung 2de5f57437 perf: Reduce Outlook Calendar & Availability Response Size (#10570)
* Getting availability reduce the response size

* Add list calendar filter param

* Clean up
2023-08-10 18:28:56 +01:00
Udit Takkar 8bdd835a68 fix: event type filter by team (#10632)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-10 18:27:02 +01:00
aku019andasishupadhyay24@gmail.com 9c7d0ea604 docs: fixed grammar in docs (#10669)
* fixed grammar and typos

* fixed grammar and typos

* docs:fixed grammar and typos

---------

Co-authored-by: asishupadhyay24@gmail.com <India@wc2019>
2023-08-10 18:23:25 +01:00
Rohit Saini a3aa53fe6e fix: #10614 - made nodemailer compatible with mailhog smtp server. (#10615)
* mailhog server auth problem solved

* put condition on transport auth

* fixe type-check error
2023-08-10 18:19:21 +01:00
57384eb921 perf: Replace un-needed context fetching (#10657)
* Replace ctx.user.credentials to fetch in their own usecases

* Remove rawavatar from return

* Remove avatar rawAvatar from handler

* Fix fallback avatars

* fix: profile.slug already includes /team

* perf: Deprecate useAvatarQuery hook

* Extract to reusable credential func

* Fix type errors for credentials

* credentialOwner was inferred incorrectly, string non-nullable

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-08-10 18:07:09 +01:00
Leo Giovanetti a153f9627d chore: Relying on CALCOM_ENV (#10098)
* Relying on CALCOM_ENV

* Restoring unneeded changes

* Relyon more on IS_PRODUCTION

* Relyon more on IS_PRODUCTION
2023-08-10 12:32:20 -04:00
GitStart-Cal.comandgitstart-calcom a54539d211 test: Create unit tests for react components in packages/ui/components/form/datepicker (#10671)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-10 12:31:52 -04:00
Udit TakkarandCarina Wollendorfer 8d736cfb67 chore: add helper text in round robin (#10674)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-10 16:30:49 +00:00
Leo Giovanetti 3c3564f884 fix: copy or view profile URL for org (#10702) 2023-08-10 17:30:33 +01:00
Alex van Andel cc75899b01 fix: count without where clause is total users regardless of selection (#10703) 2023-08-10 17:29:47 +01:00
Udit TakkarandCarinaWolli 1a818ab0c3 fix: reschedule timezone (#10697)
* fix: reschedule timezone

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

* fix: use original rescheduled booking

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

* only when organizer reschedules

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-08-10 17:05:35 +01:00
Joe Au-YeungandLeo Giovanetti 7dafaf1381 Fix redirect URLs (#10680)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-08-10 12:06:31 -03:00
Crowdin Bot 74f354abd2 New Crowdin translations by Github Action 2023-08-10 14:53:43 +00:00
Anik Dhabal Babu f281131bf6 fix: Improve onboarding Availability (#10685)
* improve onboarding

* Update Schedule.tsx
2023-08-10 14:49:41 +00:00
22b4bcdeeb fix: showing default apps by Cal like Cal-Video in installed apps (#10611)
Co-authored-by: Satya Nishanth <satya@Satyas-MacBook-Pro.local>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-08-10 16:05:26 +05:30
sean-brydon 5f59ef2acf Transform + usernames to - on token initiation (#10692) 2023-08-10 11:50:43 +02:00
sean-brydon d6d4e246f9 chore: Fix UI NITs (#10691) 2023-08-10 08:49:17 +00:00
Hariom Balharaandzomars a49c34e733 perf: Avoid unmounting of Shell on navigation and thus reduce number of paints (#10646)
Co-authored-by: zomars <zomars@me.com>
2023-08-09 15:54:51 -07:00
37ce8860b5 fix: all booking and duration limits (#10480)
Co-authored-by: rkreddy99 <rreddy@e2clouds.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2023-08-09 15:51:38 -07:00
Leo GiovanettiandOmar López 4dfbc46247 Fixing fetching team with parent (#10677)
Co-authored-by: Omar López <zomars@me.com>
2023-08-09 21:05:28 +00:00
Carina WollendorferandCarinaWolli 4c38ea3329 fix: DST issue in the month of DST change (#10664)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-08-09 13:31:48 -07:00
John Victor 24e60498f4 fix: add check mark to selected option in destionation calendar selector (#10583) 2023-08-09 20:23:47 +00:00
Udit Takkar fa9a8cca89 fix: add support email (#10605) 2023-08-09 15:57:22 -04:00
Crowdin Bot 38bb01a9f0 New Crowdin translations by Github Action 2023-08-09 19:49:47 +00:00
Omar López 3cf6fef159 chore: disable locale detection (#10676) 2023-08-09 12:46:02 -07:00
MehulandCarina Wollendorfer 26dac453e9 fix: Offset start times event setting resets after save (#10612)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-09 15:13:25 -04:00
Crowdin Bot 52db023f76 New Crowdin translations by Github Action 2023-08-09 18:36:34 +00:00
df9e02d138 fix: phone selector dropdown hidden behind logo on booking page (#10617)
Co-authored-by: mohammed gehad <mohammed.gehad.1998@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-09 18:32:28 +00:00
Crowdin Bot d7be40f67b New Crowdin translations by Github Action 2023-08-09 16:59:49 +00:00
Joe Au-YeungandLeo Giovanetti 68d2a97e92 fix: Orgs publishing with billing (#10662)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-08-09 12:56:53 -04:00
cde9b24a9b fix: Cleanup after removing connected calendar (#10012)
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-08-09 10:22:51 -04:00
Udit TakkarandSean Brydon cf89598279 fix: Calendar View (#10514)
Co-authored-by: Sean Brydon <sean@cal.com>
2023-08-09 12:19:01 +02:00
alannncandAlex van Andel fcd892bfa0 perf: no wait for session when calling getschedule 10552 cal 2311 (#10607)
* No batching on getting session

* Fix usePublicPage hook to use new router search params

* Move things so getSchedule data can be load as soon as we are rendering BookerComponent

* pre fetch session

* Removed custom code in favour of useTimePreferences

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-08-09 09:55:27 +01:00
Joe Au-Yeung bee5011ec1 fix/ Remove getUserAdminTeams endpoint (#10648) 2023-08-08 21:10:57 -04:00
Omar López e907ac8a9a v3.1.7
release.yml / release (push) Canceled after 0s
2023-08-08 16:12:09 -07:00
c22dfdd54e fix: date and month format issue on booking page for weekly view (#10512)
Co-authored-by: Raghul D <v-raghuld@microsoft.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-08-08 23:37:52 +02:00
Nadeem AshrafandUdit Takkar 2aa0188418 fix: #10553 - "Apps" child items not highlighted when selected (#10565)
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-08 23:33:58 +02:00
MontoandlemmyMwaura 59ea4d624b feat: add loading state for buttons on settings page (#10643)
* fix: add spinner to general settings section

* feat: add and manage spinner in the emptyScreen component

* feat: add spinner to two-factor auth modal buttons

* feat: add spinner to password and impersonation setting buttons

* feat: add spinner to webhookform(edit and create)

* chore: clean up code

---------

Co-authored-by: lemmyMwaura <lemmymwauracodes@gmail.com>
2023-08-08 19:32:22 +00:00
975420181d fix: smtp send email fix for self hosted instance (#10593)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-08 19:07:09 +00:00
Raju kadelandCarinaWolli 4d9a7c1b69 feat: Added loading animation in button while performing update mutation (#10622)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-08-08 19:06:09 +00:00
382cbe0a86 fix: rescheduling dynamic booking (#10597)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-08 18:51:54 +00:00
nicktrnandJoe Au-Yeung 0d1ab6ade9 fix: don't send request reminder for unpaid bookings (#10325)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-08-08 11:45:16 -04:00
Shivam Kalra 428e330301 feat: container init in app-store and embeds workflow (#10637) 2023-08-08 13:45:51 +00:00
Hosmel Quintana f5350b3a2b Fix overflow (#10641) 2023-08-08 13:51:43 +01:00
Hariom Balhara 4cd8935375 fix: SWR cache header on Vercel (#10624) 2023-08-08 13:50:30 +01:00
05020dfa9d test: Create unit tests for react components in packages/ui/components/toast (#10578)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-08 09:47:30 +01:00
Alex van AndelandCarina Wollendorfer e5f15cb1df perf: Improved buildDateRanges performance + cleanup (#10610)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-07 19:03:01 -07:00
Omar López 6b536030fa fix: API build (#10639) 2023-08-08 01:01:23 +00:00
zomars 46de1a8518 fixme: disable workflow test to unclog pipeline 2023-08-07 17:50:34 -07:00
Omar LópezandPeer Richelsen 4b78e91384 chore: upgrades to prisma v5 (#10073)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-07 22:08:13 +00:00
Crowdin Bot b9cec94880 New Crowdin translations by Github Action 2023-08-07 21:22:02 +00:00
sean-brydon 7b375ce814 perf: Correct use of next-auth session (#10638) 2023-08-07 14:19:14 -07:00
Zahid DodiyaandPeer Richelsen 29d4348e7c fix: #10587 - added a right margin to VTI. (#10613)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-07 15:16:32 +00:00
Udit TakkarandPeer Richelsen 826a8856e6 fix: installation dropdown (#10555)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-07 15:14:45 +00:00
Alex van AndelandKeith Williams 2f23b5de9b perf: Remove organisation as it's unused in getBusyTimes (?) (#10619)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-07 10:06:48 -04:00
df674532a4 fix: added bottom padding to Dialog component (#10629)
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-07 14:00:31 +00:00
Nischal Gautam 4d9047974e chore: Renamed getSchedule to getAvailableSlots (#10626)
* refactor: renamed getScheldude to getAvailableSlots

* fix ts error

* romoved comment
2023-08-07 15:00:01 +01:00
Leo Giovanetti ffa3a454bb fix: team unaccepted members (#10582) 2023-08-07 08:32:35 -03:00
Udit TakkarandHariom Balhara 0d62311b41 fix: auth redirect (#10556)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-07 16:51:02 +05:30
Moon Patel d36002d24c typo fix in app/storybook/intro.stories.mdx (#10623) 2023-08-07 15:43:21 +05:30
Crowdin Bot 7b837d48f1 New Crowdin translations by Github Action 2023-08-05 14:15:39 +00:00
sean-brydonandPeer Richelsen 8dc9f1c929 fix: Fix auto scroll (#10591)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-05 19:42:45 +05:30
Crowdin Bot b006f7f1df New Crowdin translations by Github Action 2023-08-05 06:47:19 +00:00
Alex van Andel 9a08fbb22d perf: Use Intl.DateTimeFormat instead of Dayjs.tz().format() (#10604) 2023-08-05 07:44:47 +01:00
359779cabb fix: {ATTENDEE} workflow variable always uses first attendee for for seated event (#10468)
Co-authored-by: rkreddy99 <rreddy@e2clouds.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-04 13:53:58 +00:00
d22d8095e1 fix: cancellation email for seated event for 2nd attendee onwards (#10177)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-04 09:47:16 -04:00
123ecf3700 feat: show dialog when changing email after using Google Login (#9611)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-08-03 17:26:40 -07:00
Omar López 710bfc0d7e fix: random test failure when booking are on same date (#10581) 2023-08-03 22:33:49 +00:00
Joe Au-Yeungandalannnc c7d8d435f1 fix: Only enforce min seats for orgs (#10572)
* Only check subscription count for orgs

* Undo constants change

---------

Co-authored-by: alannnc <alannnc@gmail.com>
2023-08-03 15:17:57 -04:00
Shivam KalraandKeith Williams 506ef2078f chore: mailhog (#10521)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-03 17:46:25 +01:00
Crowdin Bot e2cb1773ff New Crowdin translations by Github Action 2023-08-03 16:28:27 +00:00
Hariom Balhara 89808cb4f6 fix: Remove Email embed from Routing Form[Stacked PR] (#10389) 2023-08-03 09:25:31 -07:00
Hariom Balhara bf45dcf139 fix: More instances of wrong avatar URL fixed (#10519)
* Fix user.avatar

* Fix avtars in admin users listing

* Handle unpublished org
2023-08-03 15:32:38 +00:00
Hariom Balhara 5491821baf fix: Redact prisma errors (#10536) 2023-08-03 16:22:38 +01:00
Monto 669065cebd feat: add attendee first name and last name tokens to workflows (#10509) 2023-08-03 11:10:59 -04:00
Alex van Andel 91af873db9 perf: When a Google access token is expired and can't be refreshed, w… (#10557) 2023-08-03 13:17:38 +01:00
2de4abba65 feat: Modify sort with if/elseif/else randomize when the dates are equal (#10448)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-03 09:03:23 +00:00
Hariom Balharaandzomars 1b6d1b3a5a fix: Embed Code Generator - Fixes 404 in preview of embed for orgs (#10417)
Co-authored-by: zomars <zomars@me.com>
2023-08-02 13:03:22 -07:00
Hariom Balharaandzomars a56b19e618 fix: Broken Embed Dialog in Routing Forms due to recent rewrite (#10434)
Co-authored-by: zomars <zomars@me.com>
2023-08-02 19:40:53 +00:00
ea7853789c fix: welcome-new-contributors action (#10425)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-02 19:20:30 +00:00
alannncandOmar López 871e17a865 feat: org owner-admin cant see the teams created by other owner-admin 9982 cal 2120 (#10430)
* WIP

* New files for other teams

* Update code domain model location

* Fixes for finding teams in orgs

* VerticalTabs remove other teams appearance and only display if org admin

* Added conditional to consider orgs admin when updating team profile

* team-member-list wip

* Update memberInviteModal with disableCopyLink prop

* cleaning up

* Undo changes to files

* remove logs

* clean up

* undo changes

* undo change

* Undo changes

* Reset files to main

* typo on import

* Update packages/features/ee/organizations/pages/components/OtherTeamListItem.tsx

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

* useSearchParams hook instead of useRouter

* remove unused code and clean up

* Fix type

---------

Co-authored-by: Omar López <zomars@me.com>
2023-08-02 21:09:43 +02:00
Alex van Andel 95dee6dd37 perf: Turns out the tz() function is very slow, 4ms (#10545) 2023-08-02 13:53:19 -04:00
Alex van Andel 23f97bdd3c use dayjs utc mode to improve before/after performance (#10544) 2023-08-02 17:03:04 +00:00
Alex van Andel bf050b498c Revert "Revert "fix: weekly limits (#10404)""
This reverts commit 83678dfea2.
2023-08-02 16:27:30 +01:00
Alex van Andel 83678dfea2 Revert "fix: weekly limits (#10404)"
This reverts commit 1d5b383529.
2023-08-02 16:15:32 +01:00
Leo Giovanetti 02c5aa02df fix: random multiple duration value input [CAL-887] (#10513) 2023-08-02 10:37:59 -04:00
b7851e6e53 refactor: next/router hooks with next/navigation hooks (#9105)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-02 11:35:48 +02:00
Udit Takkar 4ca9138e01 fix: react hook can't be used conditionally (#10439) 2023-08-02 12:22:44 +05:30
nicktrn e44a07b67c fix: add booking form prefill e2e test (#10293)
* fix: add booking form prefill e2e test

* fix: booking e2e type issue

* fix: revert user fixture default name

* fix: breakage due to rebase
2023-08-02 07:40:33 +01:00
GitStart-Cal.comandgitstart-calcom 86690a4c32 test: Create unit tests for react components in packages/ui/components/list (#10501)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-02 07:13:26 +01:00
GitStart-Cal.comandgitstart-calcom c8beb6fba0 test: Create unit tests for react components in packages/ui/components/scrollable (#10489)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-02 07:11:37 +01:00
Peer Richelsen a97e145cdb Update common.json (#10507) 2023-08-02 06:52:04 +01:00
617e665004 feat: Enable Conferencing Apps for Team Events [CAL-1925] (#10383)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-02 09:24:28 +05:30
Alex van Andel a9344115d2 v3.1.6
release.yml / release (push) Canceled after 0s
2023-08-01 17:25:39 +01:00
GitStart-Cal.comandgitstart-calcom 81c11f61dc test: Create unit tests for react components in packages/ui/components/form/input (#10506)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-01 17:19:54 +01:00
Danila 7c64b6a6e2 Fix type name clashing for eslint parser (#10491) 2023-08-01 15:55:31 +00:00
17855251ff fix: Stop sending Reminders when a 'Seat' cancels the Event (#10405)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-01 10:13:28 -04:00
a0fc045852 fix: Modified event type public api hander (#9508)
* Modified event type public api hander

* Update apps/api/pages/api/event-types/_get.ts

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-08-01 13:13:24 +00:00
Prateek JainandHariom Balhara 1b0e4b0127 refactor: No name typo fix (#10493)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-01 13:02:06 +00:00
60a559940c test: Create unit tests for react components in packages/ui/components/form/daterangepicker (#10479)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-08-01 13:02:50 +01:00
Anik Dhabal Babu b893afb8b8 fix: Address translation problem (#10492) 2023-08-01 16:56:26 +05:30
6fa54aba46 perf: Refactor getEnabledApps to filter on credentials passed (#10353)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-01 16:40:52 +05:30
Crowdin Bot 88f542cb0d New Crowdin translations by Github Action 2023-08-01 08:54:53 +00:00
acc4909981 fix: embed hides floating button (#9165)
* fix: embed - hides floating button on click to prevent overlapping over modal

* revert changes on yarn.lock

* z-index for Modal wasnt highest

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-01 08:51:45 +00:00
Leo Giovanettiandzomars 4a6dc50909 fix: Unpublished screens (#10453)
* Implementation

* Changes and e2e

* Reverting launch.json

* Reverting org create handler

* Reverting yarn.lock

* DRYness and nitpicks

* Default org domain to undefined

* Applying zomars suggestion

* Suggestions

* Fixing seed and type in suggestion

* Fixing types

---------

Co-authored-by: zomars <zomars@me.com>
2023-07-31 21:27:22 +01:00
Pratik Kumar 4435451e9b Fix private link glitch (#10456) 2023-07-31 20:43:06 +01:00
12abca65b8 fix: Fixed 10080 settings sidebar scrolling issue (#10471)
* fix: #10080 settings sidebar scrolling issue

* fix: type error

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

* Fixed type error

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-31 18:51:08 +00:00
e93f1e2329 fix: added validation for empty reset password field (#10464)
* added validation for empty reset password field

* removed useState and useEffect

---------

Co-authored-by: varun thummar <Varun>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-31 19:13:45 +01:00
5a430df5d9 feat: Verify email of people setting a meeting with you (#10317)
* booker email verification changes

* name type fix

* use totp and code

* prisma schema styling

* refactor: code

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

* fix: book event form

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

* fix: type error

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

* fix: type errors

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

* fix: unit tests

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

* refactor: move verifycodedialog from ui and to features/bookings

* fix: type error

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: rkreddy99 <rreddy@e2clouds.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-07-31 17:51:11 +00:00
fcd8de43d6 fix: temporarily disable sms/whatsapp to attendee workflow action (#10485)
* disable sms and whatsapp to attendee

* fix type error

* fix prisma enum imports

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-31 17:45:48 +00:00
Alex van Andel 9498ad4916 Fixed workflowReminder test + refactored reminderSchedule (#10487) 2023-07-31 18:35:48 +01:00
Rama Krishna Reddyandrkreddy99 d21135e076 fix: do not delete last schedule (#10444)
Co-authored-by: rkreddy99 <rreddy@e2clouds.com>
2023-07-31 14:48:50 +01:00
GitStart-Cal.comandgitstart-calcom 65384d7b1f test: Create unit tests for react components in packages/ui/components/layout (#10458)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-31 12:07:21 +05:30
Neel PatelandLeo Giovanetti e68ace87e2 fix: changes for showing error message in case of INTERNAL_SERVER_ERROR in event types (#10305)
* fix: changes for showing error message in case of INTERNAL_SERVER_ERROR

* changes for edge case

---------

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-07-29 10:14:40 -03:00
sean-brydon 8f9f096723 feat: Auto join team if already in org [CAL-2248] (#10450)
* Auto join if in org.

* Only handle accepted org memberships

* Add comment

* TODO: fix prisma mock
2023-07-28 16:43:35 +00:00
Udit TakkarandPeer Richelsen 0f84102f1e perf: reduce number of api calls in event types (#10381)
* perf: reduce number of api calls in event types

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

* chore

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

* perf: use staleTime and cacheTime

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-28 17:27:44 +01:00
Keith Williams e6ae69a20f chore: Remove env file cache (#10297) 2023-07-28 08:24:53 -07:00
sean-brydon 878f074cb6 use cloudflare real IP (#10449) 2023-07-28 15:11:44 +00:00
2c2fd14050 fix: use time format (#8787)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-28 16:22:19 +02:00
Crowdin Bot 5a919acd22 New Crowdin translations by Github Action 2023-07-28 14:09:29 +00:00
Udit Takkar 063c7f6244 fix: width and avatar in orgs app install dropdown (#10447) 2023-07-28 16:04:49 +02:00
Shivam Kalra 344b9e9b2b chore: team booking test (#10440)
* feat: add team name in the createTeam fixture

* chore: remove unnecessary variables

* test: e2e booking for Round Robin EventType

* feat: teammates in user fixtures

* chore: DRY

* fix: remove redundant todo

* fix: add teamEvent title and slug in scenario

* test: booking title, RR hostname
2023-07-28 13:51:15 +01:00
Danilaandsean-brydon e8339030c2 Add optimistic update for away status toggling (#10395)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-07-28 12:18:29 +00:00
GitStart-Cal.comandgitstart-calcom 4bc4ff8193 test: Create unit tests for react components in packages/ui/components/table (#10437)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-28 12:57:44 +01:00
GitStart-Cal.com 6cff76cc3b test: Create unit tests for react components in packages/ui/components/form/colorpicker (#10426)
## DEMO

Run 
```
yarn test colorpicker
```
![image](https://github.com/calcom/cal.com/assets/121884634/31f52d82-615d-4926-bf44-0c1742a66526)
2023-07-28 09:44:52 +01:00
Crowdin Bot d47e8b48a6 New Crowdin translations by Github Action 2023-07-27 20:35:50 +00:00
Peer Richelsen 96fb3a357e fixed email embed illustraion (#10427) 2023-07-27 20:31:55 +00:00
nicktrnandUdit Takkar 1d5b383529 fix: weekly limits (#10404)
* fix: correctly apply duration limits

* fix: weekly limits for weeks in two months

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-07-27 17:33:24 +01:00
Richard PoelderlandAlex van Andel 766a64c46e chore: add datoCMS secret for handling webhooks (#10421)
* regen yarn.lock?

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

* Generated yarn.lock with submodules

* Added yarn.lock auth

* add datocms webhook secret

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-27 16:01:27 +00:00
Anik Dhabal Babu 65600b76f2 fix: Set the limit of Occurrences (#10269) 2023-07-27 11:49:21 -04:00
Cherishandcherish2003 0c37f8a712 fix: Booking Confirmation Dialogbox (#10397)
Co-authored-by: cherish2003 <saicherissh90@gmail.com>
2023-07-27 11:31:15 -04:00
Alex van Andel aa21217cfb chore: Remove extra params from the users prop. (#10411)
* Removes extra params from the users prop.

* context no longer requires explicit type

* Remove user.avatar prop
2023-07-27 16:06:39 +02:00
Alex van Andel 32068a646e Remove IP ban and tweak matcher accordingly (#10418) 2023-07-27 15:02:16 +01:00
Crowdin Bot 697e80966b New Crowdin translations by Github Action 2023-07-27 13:55:50 +00:00
sajanlamsal 35d55b2960 fix: changed Japanese translations on booking page to better match the scenario. (#10416) 2023-07-27 15:52:34 +02:00
Crowdin Bot c1a562af97 New Crowdin translations by Github Action 2023-07-27 13:52:03 +00:00
e81985c80d test: Create unit tests for react components in packages/ui/components/TokenHandle (#10407)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-07-27 15:48:23 +02:00
GitStart-Cal.comandgitstart-calcom a1e6e8ff32 Update package.json (#10045)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-27 15:47:48 +02:00
GitStart-Cal.comandgitstart-calcom 54cf49cfd6 test: Create unit tests for react components in packages/ui/components/form/checkbox (#10409)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-27 15:46:55 +02:00
Shivam Kalra e4a075b5e2 test: Round Robin Team Booking (#10413) 2023-07-27 17:18:08 +05:30
Peer Richelsen 660d28b286 Update README.md 2023-07-27 13:47:08 +02:00
Crowdin Bot 2b1c600d99 New Crowdin translations by Github Action 2023-07-27 11:21:03 +00:00
Hariom Balhara 48b79b618f fix: middleware logic Routing Forms (#10412)
* Match /apps/routing_forms

* Next.js config.matcher doesnt support spread operator
2023-07-27 12:18:17 +01:00
98a156311d feat: Add full names in the description of dynamic events. (#10251)
* Add full names in the descripiton of dynamic events.

* Initialising dynamic event desc after the check.

* Revert "Initialising dynamic event desc after the check."

This reverts commit ddd89c4e252f439a5875bfc34b20050a29b68de8.

* Revert "Add full names in the descripiton of dynamic events."

This reverts commit ce232d6256b4fe03c5b0665133be6ea524349c5c.

* Added booking title for dynamic events.

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

* Fix typo.

* Fix lint error.

* Fix redundant booking information.

* Fix type script lint.

* Addressed review suggestions.

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-27 08:52:46 +00:00
sean-brydon 8cdddfa1d0 fix: ratelimit - updates (#10347) 2023-07-27 10:41:43 +02:00
Udit TakkarandKeith Williams 4f488be654 fix: email update bug (#10306)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-27 10:41:33 +02:00
cdb78b686b fix: spelling errors (#10376)
* fixed spelling errors

* chore: undo cancelled

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-07-27 10:41:03 +02:00
Hariom Balharaandzomars f1acedff77 fix: Make routing-form a preinstalled app (#9836)
Co-authored-by: zomars <zomars@me.com>
2023-07-26 16:44:19 +00:00
Alex van Andel 9c389c307c v3.1.5
release.yml / release (push) Canceled after 0s
2023-07-26 17:37:11 +01:00
Leo Giovanetti dae4627ada chore: Skipping totp code for org dev (#10403) 2023-07-26 18:15:37 +02:00
Hariom BalharaandJoe Au-Yeung 83d98a1ac4 fix: Apps (#10394)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-07-26 17:43:33 +02:00
Leo Giovanetti bd40dcfa4d fix: Missing subteam URL [CAL-2240] (#10399) 2023-07-26 17:41:38 +02:00
nicktrn 9dc4c84cfd fix: make husky install compatible with yarn v3 (#10393) 2023-07-26 13:00:32 +01:00
Udit TakkarandAlex van Andel 69c01b69f6 fix: responsive upload avatar (#10392)
* fix: responsive upload avatar

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

* Update ImageUploader.tsx

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

---------

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

* Refactor

* fix usernames

* Return a response so the request does not stall

* Fix conditional

---------

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

* Update showToast.tsx

* Adjust E2E test for toast

---------

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

* feat: dynamic group links

* fix: type errors

* fix: more type errors

* Booker components cannot rely on useRouter

* Rename 15min/30/60 to dynamic

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

---------

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

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

* Generated yarn.lock with submodules

* Added yarn.lock auth

---------

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

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

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

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

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

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

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

* Fix and comment

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

* add first e2e test

* finish first e2e test

* code clean up

* fix event type create button data-testid

* enable sandbox mode for non production environments

* comment out code for testing

* Revert "comment out code for testing"

This reverts commit 5553dacce23cd85c27e41b76f99fc31ce69499b7.

* clean up comments

* prettier

* use NEXT_PUBLIC_IS_E2E

* use todo fn

---------

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

* Release latest embeds

* Ensure that unpublished packages arent imported

* linter is important to catch unexpected imports

* Create new release

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

* fix: dialog in webhook, otp

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

* fix: more dialogs

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

* fix: margin

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

---------

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

* Missing change

* Missing logic for converted providers

* Fixing edge case and applying feedback

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

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

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

* Discard changes to .vscode/launch.json

* revert yarn.lock

---------

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

* Share propsType across fieldTypes and components

* Simplify Components

* Add assertions for required field indicator

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

* Make it disabled during reschedule

* Fix UI issues in dark mode

* Support adding links in boolean checkbox

* Revert "Support adding links in boolean checkbox"

This reverts commit 31252f8a5f445f8289a09445124109a4a9c3d33b.

* Make sure getBookingFields isnt import client side

* PR feedback addressed from Carina

* DRY Code

* Fix Dialog

---------

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

shoutout @shadcn for the base of this table <3 

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

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

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

* add test

---------

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

Fixes #10032

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


## Type of change

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

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

## How should this be tested?

- See loom

## Mandatory Tasks

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

## Checklist
- I haven't added tests that prove my fix is effective or that my feature works
2023-07-18 09:46:35 -07:00
Hariom Balhara 34d65f67a9 fix: Avoid resetting state one every render. (#10210) 2023-07-18 16:42:40 +00:00
Keith Williams e4c49d0e0b fix: Added step to create env file for bundle analysis (#10224) 2023-07-18 09:20:17 -07:00
680 changed files with 21618 additions and 8807 deletions
+6 -6
View File
@@ -22,18 +22,18 @@ Any other relevant information. For example, why do you consider this a bug and
### Actual Results
- What's happening right now that is different from what is expected
- What's happening right now that is different from what is expected
### Expected Results
- This is an ideal result that the system should get after the tests are performed
- This is an ideal result that the system should get after the tests are performed
### Technical details
- Browser version, screen recording, console logs, network requests: You can make a recording with [Bird Eats Bug](https://birdeatsbug.com/).
- Node.js version
- Anything else that you think could be an issue.
- Browser version, screen recording, console logs, network requests: You can make a recording with [Bird Eats Bug](https://birdeatsbug.com/).
- Node.js version
- Anything else that you think could be an issue.
### Evidence
- How was this tested? This is quite mandatory in terms of bugs. Providing evidence of your testing with screenshots or/and videos is an amazing way to prove the bug and a troubleshooting chance to find the solution.
- How was this tested? This is quite mandatory in terms of bugs. Providing evidence of your testing with screenshots or/and videos is an amazing way to prove the bug and a troubleshooting chance to find the solution.
+13 -12
View File
@@ -11,31 +11,32 @@ Fixes # (issue)
## Requirement/Documentation
<!-- Please provide all documents that are important to understand the reason of that PR. -->
- If there is a requirement document, please, share it here.
- If there is ab UI/UX design document, please, share it here.
- If there is a requirement document, please, share it here.
- If there is ab UI/UX design document, please, share it here.
## Type of change
<!-- Please delete bullets that are not relevant. -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
## How should this be tested?
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests -->
- Are there environment variables that should be set?
- What are the minimal test data to have?
- What is expected (happy path) to have (input and output)?
- Any other important info that could help to test that PR
- Are there environment variables that should be set?
- What are the minimal test data to have?
- What is expected (happy path) to have (input and output)?
- Any other important info that could help to test that PR
## Mandatory Tasks
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
## Checklist
-17
View File
@@ -1,17 +0,0 @@
name: "Set environment variables"
description: "Configures environment variables for a workflow"
runs:
using: "composite"
steps:
- name: Read env file
uses: buildjet/cache@v3
id: env-cache
with:
path: gh.env-${{ github.sha }}
key: env-cache
restore-keys: env-cache-
- name: Set Environment Variables
uses: tw3lveparsecs/github-actions-setvars@latest
with:
envFilePath: gh.env-${{ github.sha }}
+35 -4
View File
@@ -5,7 +5,7 @@ on:
jobs:
e2e-app-store:
timeout-minutes: 20
name: E2E App-Store Apps
name: E2E App Store (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: buildjet-4vcpu-ubuntu-2204
services:
postgres:
@@ -15,6 +15,16 @@ jobs:
POSTGRES_DB: calendso
ports:
- 5432:5432
mailhog:
image: mailhog/mailhog:v1.0.1
ports:
- 8025:8025
- 1025:1025
strategy:
fail-fast: false
matrix:
## There aren't many tests for AppStore. So, just start with 2 shards. Increase if needed.
shard: [1, 2]
steps:
- uses: actions/checkout@v3
@@ -22,16 +32,37 @@ jobs:
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- uses: ./.github/actions/env-read-file
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn test-e2e:app-store
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results
name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: test-results
+31 -4
View File
@@ -1,10 +1,11 @@
name: E2E Embed tests and booking flow(for non-embed as well)
name: E2E Embed React tests and booking flow(for non-embed as well)
on:
workflow_call:
jobs:
e2e-embed:
timeout-minutes: 20
name: E2E Embed React (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: buildjet-4vcpu-ubuntu-2204
services:
postgres:
@@ -14,6 +15,11 @@ jobs:
POSTGRES_DB: calendso
ports:
- 5432:5432
strategy:
fail-fast: false
matrix:
## There aren't many tests for embed-react. So, just start with 2 shards. Increase if needed.
shard: [1, 2]
steps:
- uses: actions/checkout@v3
@@ -21,18 +27,39 @@ jobs:
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- uses: ./.github/actions/env-read-file
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
- name: Run Tests
run: |
yarn test-e2e:embed-react
yarn e2e:embed-react --shard=${{ matrix.shard }}/${{ strategy.job-total }}
yarn workspace @calcom/embed-react packaged:tests
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results
name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: test-results
+36 -4
View File
@@ -1,10 +1,11 @@
name: E2E Embed tests and booking flow(for non-embed as well)
name: E2E Embed Core tests and booking flow(for non-embed as well)
on:
workflow_call:
jobs:
e2e-embed:
timeout-minutes: 20
name: E2E Embed Core (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: buildjet-4vcpu-ubuntu-2204
services:
postgres:
@@ -14,6 +15,16 @@ jobs:
POSTGRES_DB: calendso
ports:
- 5432:5432
mailhog:
image: mailhog/mailhog:v1.0.1
ports:
- 8025:8025
- 1025:1025
strategy:
fail-fast: false
matrix:
## There aren't many tests for embed-core. So, just start with 2 shards. Increase if needed.
shard: [1, 2]
steps:
- uses: actions/checkout@v3
@@ -21,16 +32,37 @@ jobs:
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- uses: ./.github/actions/env-read-file
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn test-e2e:embed
run: yarn e2e:embed --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results
name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: test-results
+33 -1
View File
@@ -1,6 +1,8 @@
name: E2E test
on:
workflow_call:
jobs:
e2e:
timeout-minutes: 20
@@ -14,6 +16,11 @@ jobs:
POSTGRES_DB: calendso
ports:
- 5432:5432
mailhog:
image: mailhog/mailhog:v1.0.1
ports:
- 8025:8025
- 1025:1025
strategy:
fail-fast: false
matrix:
@@ -24,13 +31,38 @@ jobs:
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- uses: ./.github/actions/env-read-file
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
-55
View File
@@ -1,55 +0,0 @@
name: Create .env file
on:
workflow_call:
env:
ENV_FILE_NAME: gh.env-${{ github.sha }}
INPUT_ENV_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
INPUT_ENV_NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
INPUT_ENV_NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
INPUT_ENV_NEXTAUTH_SECRET: secret
INPUT_ENV_GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
INPUT_ENV_GOOGLE_LOGIN_ENABLED: true
# INPUT_ENV_CRON_API_KEY: xxx
# INPUT_ENV_CRON_ENABLE_APP_SYNC: true|false
INPUT_ENV_CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
INPUT_ENV_NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
INPUT_ENV_STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
INPUT_ENV_STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
INPUT_ENV_STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
INPUT_ENV_PAYMENT_FEE_PERCENTAGE: 0.005
INPUT_ENV_PAYMENT_FEE_FIXED: 10
INPUT_ENV_SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
INPUT_ENV_SAML_ADMINS: pro@example.com
INPUT_ENV_NEXTAUTH_URL: http://localhost:3000
INPUT_ENV_NEXT_PUBLIC_IS_E2E: 1
# INPUT_ENV_EMAIL_FROM: e2e@cal.com
# INPUT_ENV_EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
# INPUT_ENV_EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
# INPUT_ENV_EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
# INPUT_ENV_EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
# INPUT_ENV_MS_GRAPH_CLIENT_ID: xxx
# INPUT_ENV_MS_GRAPH_CLIENT_SECRET: xxx
# INPUT_ENV_ZOOM_CLIENT_ID: xxx
# INPUT_ENV_ZOOM_CLIENT_SECRET: xxx
INPUT_ENV_SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
INPUT_ENV_SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
INPUT_ENV_TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
INPUT_ENV_TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
create_env_file:
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ozaytsev86/create-env-file@v1
with:
file-name: ${{ github.workspace }}/${{ env.ENV_FILE_NAME }}
- uses: buildjet/cache@v3
id: env-cache
with:
path: ${{ env.ENV_FILE_NAME }}
key: env-cache-${{ hashFiles(env.ENV_FILE_NAME) }}
+1 -11
View File
@@ -44,11 +44,6 @@ jobs:
- 'packages/embeds/**'
- 'playwright.config.ts'
env:
name: Create env file
uses: ./.github/workflows/env-create-file.yml
secrets: inherit
type-check:
name: Type check
uses: ./.github/workflows/check-types.yml
@@ -66,13 +61,11 @@ jobs:
build:
name: Production build
needs: env
uses: ./.github/workflows/production-build.yml
secrets: inherit
build-without-database:
name: Production build (without database)
needs: env
uses: ./.github/workflows/production-build-without-database.yml
secrets: inherit
@@ -84,21 +77,18 @@ jobs:
e2e-app-store:
name: E2E App Store tests
if: ${{ needs.changes.outputs.app-store == 'true' }}
needs: [changes, lint, build]
uses: ./.github/workflows/e2e-app-store.yml
secrets: inherit
e2e-embed:
name: E2E embeds tests
if: ${{ needs.changes.outputs.embed == 'true' }}
needs: [changes, lint, build]
uses: ./.github/workflows/e2e-embed.yml
secrets: inherit
e2e-embed-react:
name: E2E React embeds tests
if: ${{ needs.changes.outputs.embed-react == 'true' }}
needs: [changes, lint, build]
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
@@ -109,7 +99,7 @@ jobs:
secrets: inherit
required:
needs: [lint, type-check, test, build, e2e]
needs: [lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
if: always()
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
@@ -3,6 +3,30 @@ name: Production Build (without database)
on:
workflow_call:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
build:
name: Build
@@ -12,5 +36,4 @@ jobs:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/env-read-file
- uses: ./.github/actions/cache-build
+24 -1
View File
@@ -3,6 +3,30 @@ name: Production Build
on:
workflow_call:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
build:
name: Build
@@ -20,6 +44,5 @@ jobs:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/env-read-file
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
+47
View File
@@ -0,0 +1,47 @@
name: "Release Docker"
on: # yamllint disable-line rule:truthy
release:
types:
- created
# in case manual trigger is needed
workflow_dispatch:
inputs:
RELEASE_TAG:
description: 'v{Major}.{Minor}.{Patch}'
jobs:
release:
name: "Remote Release"
runs-on: "ubuntu-latest"
steps:
- name: checkout
uses: actions/checkout@v3
- name: "Determine tag"
run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV"
- name: "Run remote release workflow"
uses: "actions/github-script@v6"
with:
# Requires a personal access token with Actions Read and write permissions on calcom/docker.
github-token: "${{ secrets.DOCKER_REPO_ACCESS_TOKEN }}"
script: |
try {
const response = await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: 'docker',
workflow_id: 'create-release.yaml',
ref: 'main',
inputs: {
"RELEASE_TAG": process.env.RELEASE_TAG
},
});
console.log(response);
} catch (error) {
console.error(error);
core.setFailed(error.message);
}
+18 -19
View File
@@ -1,28 +1,27 @@
name: "Welcome new contributors"
on:
issues:
types: opened
pull_request:
types: opened
issues:
types: opened
pull_request:
types: opened
permissions:
pull-requests: write
issues: write
jobs:
welcome-message:
name: Welcoming New Users
runs-on: ubuntu-latest
timeout-minutes: 10
needs: semantics
if: github.event.action == 'opened'
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: |-
Thank you for making your first Pull Request and taking the time to improve Cal.com ! ❤️🎉
Feel free to join the conversation at [discord](https://go.cal.com/discord)
issue-message: |
Thank you for opening your first issue, one of our team members will review it as soon as it possible. ❤️🎉
welcome-message:
name: Welcoming New Users
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.event.action == 'opened'
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: |-
Thank you for making your first Pull Request and taking the time to improve Cal.com ! ❤️🎉
Feel free to join the conversation at [discord](https://go.cal.com/discord)
issue-message: |
Thank you for opening your first issue, one of our team members will review it as soon as it possible. ❤️🎉
+5 -8
View File
@@ -51,9 +51,9 @@
# Scheduling infrastructure for absolutely everyone
The open source Calendly alternative. You are in charge
of your own data, workflow and appearance.
of your own data, workflow, and appearance.
Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes and even calls with our families. However, most tools are very limited in terms of control and customizations.
Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes, and even calls with our families. However, most tools are very limited in terms of control and customization.
That's where Cal.com comes in. Self-hosted or hosted by us. White-label by design. API-driven and ready to be deployed on your own domain. Full control of your events and data.
@@ -90,7 +90,6 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
- [Prisma.io](https://prisma.io/?ref=cal.com)
- [Daily.co](https://go.cal.com/daily)
## Contact us
Meet our sales team for any commercial inquiries.
@@ -99,7 +98,7 @@ Meet our sales team for any commercial inquiries.
## Stay Up-to-Date
Cal.com officially launched as v.1.0 on 15th of September 2021 and we've come a long way so far. Watch **releases** of this repository to be notified of future updates:
Cal.com officially launched as v.1.0 on the 15th of September 2021 and we've come a long way so far. Watch **releases** of this repository to be notified of future updates:
![cal-star-github](https://user-images.githubusercontent.com/8019099/154853944-a9e3c999-3da3-4048-b149-b4f73893c6fb.gif)
@@ -129,7 +128,7 @@ Here is what you need to be able to run Cal.com.
git clone https://github.com/calcom/cal.com.git
```
> If you are on windows, run the following command on `gitbash` with admin privileges: <br> > `git clone -c core.symlinks=true https://github.com/calcom/cal.com.git` <br>
> If you are on Windows, run the following command on `gitbash` with admin privileges: <br> > `git clone -c core.symlinks=true https://github.com/calcom/cal.com.git` <br>
> See [docs](https://cal.com/docs/how-to-guides/how-to-troubleshoot-symbolic-link-issues-on-windows#enable-symbolic-links) for more details.
1. Go to the project folder
@@ -255,7 +254,7 @@ Be sure to set the environment variable `NEXTAUTH_URL` to the correct value. If
# In a terminal just run:
yarn test-e2e
# To open last HTML report run:
# To open the last HTML report run:
yarn playwright show-report test-results/reports/playwright-html-report
```
@@ -546,6 +545,4 @@ Special thanks to these amazing projects which help power Cal.com:
- [Tailwind CSS](https://tailwindcss.com/)
- [Prisma](https://prisma.io/)
<a href="https://jitsu.com/?utm_source=cal.com-gihub"><img height="40px" src="https://jitsu.com/img/powered-by-jitsu.png?gh=true" alt="Jitsu.com"></a>
Cal.com is an [open startup](https://cal.com/open) and [Jitsu](https://github.com/jitsucom/jitsu) (an open-source Segment alternative) helps us to track most of the usage metrics.
+1 -1
View File
@@ -7,7 +7,7 @@
<a href="https://cal.com/docs/enterprise-features/api#api-server-specifications">Read the API docs</a>
</div>
# Commercial Cal.com Public API
# Commercial Cal.com Public API
Welcome to the Public API ("/apps/api") of the Cal.com.
+7 -1
View File
@@ -1,6 +1,8 @@
import * as Sentry from "@sentry/nextjs";
import type { NextMiddleware } from "next-api-middleware";
import { redactError } from "@calcom/lib/redactError";
export const captureErrors: NextMiddleware = async (_req, res, next) => {
try {
// Catch any errors that are thrown in remaining
@@ -8,7 +10,11 @@ export const captureErrors: NextMiddleware = async (_req, res, next) => {
await next();
} catch (error) {
Sentry.captureException(error);
console.log(error);
const redactedError = redactError(error);
if (redactedError instanceof Error) {
res.status(400).json({ message: redactedError.message, error: redactedError });
return;
}
res.status(400).json({ message: "Something went wrong", error });
}
};
+2 -1
View File
@@ -2,6 +2,7 @@ import type { NextMiddleware } from "next-api-middleware";
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { isAdminGuard } from "~/lib/utils/isAdmin";
@@ -17,7 +18,7 @@ export const dateNotInPast = function (date: Date) {
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
const { prisma, isCustomPrisma, isAdmin } = req;
const hasValidLicense = await checkLicense(prisma);
if (!hasValidLicense && process.env.NODE_ENV !== "development")
if (!hasValidLicense && IS_PRODUCTION)
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
// If the user is an admin and using a license key (from customPrisma), skip the apiKey check.
if (isCustomPrisma && isAdmin) {
+10 -3
View File
@@ -28,10 +28,17 @@ const withMiddleware = label(
customPrismaClient,
extendRequest,
pagination: withPagination,
sentry: captureErrors,
captureErrors,
},
// The order here, determines the order of execution, put customPrismaClient before verifyApiKey always.
["extendRequest", "sentry", "customPrismaClient", "verifyApiKey", "addRequestId"] // <-- Provide a list of middleware to call automatically
// The order here, determines the order of execution
[
"extendRequest",
"captureErrors",
// - Put customPrismaClient before verifyApiKey always.
"customPrismaClient",
"verifyApiKey",
"addRequestId",
] // <-- Provide a list of middleware to call automatically
);
export { withMiddleware };
-3
View File
@@ -2,7 +2,6 @@ import type { NextApiRequest } from "next";
import { z } from "zod";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { availabilityUserSelect } from "@calcom/prisma";
@@ -120,10 +119,8 @@ const availabilitySchema = z
async function handler(req: NextApiRequest) {
const { prisma, isAdmin, userId: reqUserId } = req;
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
if (!teamId)
return getUserAvailability({
orgSlug: isValidOrgDomain ? currentOrgDomain || undefined : undefined,
username,
dateFrom,
dateTo,
+8 -1
View File
@@ -57,7 +57,14 @@ import { defaultResponder } from "@calcom/lib/server";
* format: email
* description: 'Attendee email address'
* location:
* type: string
* type: object
* properties:
* optionValue:
* type: string
* description: 'Option value for the location'
* value:
* type: string
* description: 'The meeting URL, Phone number or Address'
* description: 'Meeting location'
* metadata:
* type: object
+1
View File
@@ -56,6 +56,7 @@ async function getHandler(req: NextApiRequest) {
owner: { select: { username: true, id: true } },
},
});
if (data.length === 0) new HttpError({ statusCode: 404, message: "No event types were found" });
return {
event_types: data.map((eventType) => {
const link = getCalLink(eventType);
+1 -1
View File
@@ -40,7 +40,7 @@ export async function getHandler(req: NextApiRequest) {
// If user is not ADMIN, return only his data.
if (!isAdmin) where.id = userId;
const [total, data] = await prisma.$transaction([
prisma.user.count(),
prisma.user.count({ where }),
prisma.user.findMany({ where, take, skip }),
]);
const users = schemaUsersReadPublic.parse(data);
+1 -1
View File
@@ -4,7 +4,7 @@ import { Meta } from '@storybook/addon-docs';
<div className="text-center flex flex-col items-center">
<h1 style={{marginBottom: '24px', marginTop: '36px'}}>Welcome to Cal.com UI</h1>
<p>This is the beginning of our storybook impovments to match Figma as close as possible. Like our Figma library, we will be adding more components as we go along.</p>
<p>This is the beginning of our storybook improvements to match Figma as close as possible. Like our Figma library, we will be adding more components as we go along.</p>
<p>Our <a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">Figma</a> library is avalible for anyone to view and use. If you have any questions or concerns, please reach out to the design team.</p>
</div>
+34
View File
@@ -1,5 +1,39 @@
# @calcom/web
## 3.1.3
### Patch Changes
- Updated dependencies
- @calcom/embed-react@1.3.0
## 3.0.10
### Patch Changes
- Updated dependencies
- @calcom/embed-snippet@1.1.2
- @calcom/embed-react@1.2.2
- @calcom/embed-core@1.3.2
## 3.0.9
### Patch Changes
- Updated dependencies
- @calcom/embed-snippet@1.1.1
- @calcom/embed-react@1.2.1
- @calcom/embed-core@1.3.1
## 3.0.8
### Patch Changes
- Updated dependencies
- @calcom/embed-core@1.3.0
- @calcom/embed-react@1.2.0
- @calcom/embed-snippet@1.1.0
## 2.9.4
### Patch Changes
+17 -6
View File
@@ -1,15 +1,24 @@
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import type { ReactNode } from "react";
import { useEffect, useRef, useState } from "react";
import { z } from "zod";
import type { CredentialOwner } from "@calcom/app-store/types";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { Badge, ListItemText, Avatar } from "@calcom/ui";
import { AlertCircle } from "@calcom/ui/components/icon";
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
type ShouldHighlight =
| {
slug: string;
shouldHighlight: true;
}
| {
shouldHighlight?: never;
slug?: never;
};
type AppListCardProps = {
logo?: string;
@@ -46,14 +55,16 @@ export default function AppListCard(props: AppListCardProps) {
const router = useRouter();
const [highlight, setHighlight] = useState(shouldHighlight && hl === slug);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const searchParams = useSearchParams();
const pathname = usePathname();
useEffect(() => {
if (shouldHighlight && highlight) {
const timer = setTimeout(() => {
setHighlight(false);
const url = new URL(window.location.href);
url.searchParams.delete("hl");
router.replace(url.pathname, undefined, { shallow: true });
const _searchParams = new URLSearchParams(searchParams);
_searchParams.delete("hl");
router.replace(`${pathname}?${_searchParams.toString()}`);
}, 3000);
timeoutRef.current = timer;
}
@@ -96,7 +107,7 @@ export default function AppListCard(props: AppListCardProps) {
className="mr-2"
alt={credentialOwner.name || "Nameless"}
size="xs"
imageSrc={credentialOwner.avatar}
imageSrc={getPlaceholderAvatar(credentialOwner.avatar, credentialOwner?.name as string)}
/>
{credentialOwner.name}
</div>
-26
View File
@@ -1,26 +0,0 @@
import { useSession } from "next-auth/react";
import React from "react";
import NavTabs from "./NavTabs";
const tabs = [
{
name: "app_store",
href: "/apps",
},
{
name: "installed_apps",
href: "/apps/installed",
},
];
export default function AppsShell({ children }: { children: React.ReactNode }) {
const { status } = useSession();
return (
<>
<div className="mb-12 block lg:hidden">{status === "authenticated" && <NavTabs tabs={tabs} />}</div>
<main className="pb-6">{children}</main>
</>
);
}
-31
View File
@@ -1,31 +0,0 @@
import React from "react";
import NavTabs from "./NavTabs";
const tabs = [
{
name: "upcoming",
href: "/bookings/upcoming",
},
{
name: "recurring",
href: "/bookings/recurring",
},
{
name: "past",
href: "/bookings/past",
},
{
name: "cancelled",
href: "/bookings/cancelled",
},
];
export default function BookingsShell({ children }: { children: React.ReactNode }) {
return (
<>
<NavTabs tabs={tabs} />
<main>{children}</main>
</>
);
}
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -1,4 +1,3 @@
import { isEmpty } from "lodash";
import { useTranslation } from "next-i18next";
import { useEffect } from "react";
@@ -20,13 +19,13 @@ export function useViewerI18n() {
/**
* Auto-switches locale client-side to the logged in user's preference
*/
const I18nLanguageHandler = (): null => {
const I18nLanguageHandler = () => {
const { i18n } = useTranslation("common");
const locale = useViewerI18n().data?.locale || i18n.language;
useEffect(() => {
// bail early when i18n = {}
if (isEmpty(i18n)) return;
if (Object.keys(i18n).length === 0) return;
// if locale is ready and the i18n.language does != locale - changeLanguage
if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale);
@@ -35,7 +34,6 @@ const I18nLanguageHandler = (): null => {
document.dir = i18n.dir();
document.documentElement.setAttribute("lang", locale);
}, [locale, i18n]);
return null;
};
-99
View File
@@ -1,99 +0,0 @@
import { noop } from "lodash";
import type { LinkProps } from "next/link";
import Link from "next/link";
import { useRouter } from "next/router";
import type { FC, MouseEventHandler } from "react";
import { Fragment } from "react";
import { PermissionContainer } from "@calcom/features/auth/PermissionContainer";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@lib/classNames";
import type { SVGComponent } from "@lib/types/SVGComponent";
export interface NavTabProps {
tabs: {
name: string;
/** If you want to change the path as per current tab */
href?: string;
/** If you want to change query param tabName as per current tab */
tabName?: string;
icon?: SVGComponent;
adminRequired?: boolean;
className?: string;
}[];
linkProps?: Omit<LinkProps, "href">;
}
const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
const router = useRouter();
const { t } = useLocale();
return (
<>
<nav
className="no-scrollbar -mb-px flex space-x-5 overflow-x-scroll rtl:space-x-reverse sm:rtl:space-x-reverse"
aria-label="Tabs"
{...props}>
{tabs.map((tab) => {
if ((tab.tabName && tab.href) || (!tab.tabName && !tab.href)) {
throw new Error("Use either tabName or href");
}
let href = "";
let isCurrent;
if (tab.href) {
href = tab.href;
isCurrent = router.asPath === tab.href;
} else if (tab.tabName) {
href = "";
isCurrent = router.query.tabName === tab.tabName;
}
const onClick: MouseEventHandler = tab.tabName
? (e) => {
e.preventDefault();
router.push({
query: {
...router.query,
tabName: tab.tabName,
},
});
}
: noop;
const Component = tab.adminRequired ? PermissionContainer : Fragment;
const className = tab.className || "";
return (
<Component key={tab.name}>
<Link key={tab.name} href={href} {...linkProps} legacyBehavior>
<a
onClick={onClick}
className={classNames(
isCurrent
? "text-emphasis border-gray-900"
: "hover:border-default hover:text-default text-subtle border-transparent",
"group inline-flex items-center border-b-2 px-1 py-4 text-sm font-medium",
className
)}
aria-current={isCurrent ? "page" : undefined}>
{tab.icon && (
<tab.icon
className={classNames(
isCurrent ? "text-emphasis" : "group-hover:text-subtle text-muted",
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
)}
aria-hidden="true"
/>
)}
<span>{t(tab.name)}</span>
</a>
</Link>
</Component>
);
})}
</nav>
<hr className="border-subtle" />
</>
);
};
export default NavTabs;
-60
View File
@@ -1,60 +0,0 @@
import type { ComponentProps } from "react";
import React from "react";
import Shell from "@calcom/features/shell/Shell";
import { ErrorBoundary } from "@calcom/ui";
import { CreditCard, Key, Lock, Terminal, User, Users } from "@calcom/ui/components/icon";
import NavTabs from "./NavTabs";
const tabs = [
{
name: "profile",
href: "/settings/my-account/profile",
icon: User,
},
{
name: "teams",
href: "/settings/teams",
icon: Users,
},
{
name: "security",
href: "/settings/security",
icon: Key,
},
{
name: "developer",
href: "/settings/developer",
icon: Terminal,
},
{
name: "billing",
href: "/settings/billing",
icon: CreditCard,
},
{
name: "admin",
href: "/settings/admin",
icon: Lock,
adminRequired: true,
},
];
export default function SettingsShell({
children,
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
return (
<Shell {...rest} hideHeadingOnMobile>
<div className="sm:mx-auto">
<NavTabs tabs={tabs} />
</div>
<main className="max-w-4xl">
<>
<ErrorBoundary>{children}</ErrorBoundary>
</>
</main>
</Shell>
);
}
+6 -469
View File
@@ -1,376 +1,17 @@
import Link from "next/link";
import type { IframeHTMLAttributes } from "react";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/router";
import { CAL_URL } from "@calcom/lib/constants";
import type { RouterOutputs } from "@calcom/trpc/react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import Shell from "@calcom/features/shell/Shell";
import classNames from "@calcom/lib/classNames";
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { App as AppType } from "@calcom/types/App";
import type { ButtonProps } from "@calcom/ui";
import type { AppFrontendPayload as App } from "@calcom/types/App";
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
import {
Dropdown,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuPortal,
DropdownMenuLabel,
DropdownItem,
Avatar,
} from "@calcom/ui";
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon";
import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner";
import { HeadSeo } from "@calcom/ui";
/* These app slugs all require Google Cal to be installed */
const Component = ({
name,
type,
logo,
slug,
variant,
body,
categories,
author,
price = 0,
commission,
isGlobal = false,
feeType,
docs,
website,
email,
tos,
privacy,
teamsPlanRequired,
descriptionItems,
isTemplate,
dependencies,
}: Parameters<typeof App>[0]) => {
const { t, i18n } = useLocale();
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
const mutation = useAddAppMutation(null, {
onSuccess: (data) => {
if (data?.setupPending) return;
showToast(t("app_successfully_installed"), "success");
},
onError: (error) => {
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
},
});
const priceInDollar = Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price);
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
const [showDisconnectIntegration, setShowDisconnectIntegration] = useState(false);
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery(
{ appType: type },
{
onSettled(data) {
const credentialsCount = data?.credentials.length || 0
setShowDisconnectIntegration(data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0);
setExistingCredentials(data?.credentials.map(credential => credential.id) || []);
},
}
);
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
enabled: !!dependencies,
});
const disableInstall =
dependencyData.data && dependencyData.data.some((dependency) => !dependency.installed);
// const disableInstall = requiresGCal && !gCalInstalled.data;
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
// Such apps, can only be installed once.
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
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">
{hasDescriptionItems && (
<div className="align-center bg-subtle -ml-4 -mr-4 mb-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap p-4 md:-ml-8 md:-mr-8 md:mb-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
{descriptionItems ? (
descriptionItems.map((descriptionItem, index) =>
typeof descriptionItem === "object" ? (
<div
key={`iframe-${index}`}
className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full">
<iframe allowFullScreen {...descriptionItem.iframe} />
</div>
) : (
<img
key={descriptionItem}
src={descriptionItem}
alt={`Screenshot of app ${name}`}
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
/>
)
)
) : (
<SkeletonText />
)}
</div>
)}
<div
className={classNames(
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
hasDescriptionItems && "lg:ml-8"
)}>
<div className="mb-8 flex pt-4">
<header>
<div className="mb-4 flex items-center">
<img
className={classNames(logo.includes("-dark") && "dark:invert", "min-h-16 min-w-16 h-16 w-16")}
src={logo}
alt={name}
/>
<h1 className="font-cal text-emphasis ml-4 text-3xl">{name}</h1>
</div>
<h2 className="text-default text-sm font-medium">
<Link
href={`categories/${categories[0]}`}
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
{categories[0]}
</Link>{" "}
•{" "}
<a target="_blank" rel="noreferrer" href={website}>
{t("published_by", { author })}
</a>
</h2>
{isTemplate && (
<Badge variant="red" className="mt-4">
Template - Available in Dev Environment only for testing
</Badge>
)}
</header>
</div>
{!appDbQuery.isLoading ? (
isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3">
<Button StartIcon={Check} color="secondary" disabled>
{existingCredentials.length > 0
? t("active_install", { count: existingCredentials.length })
: t("default")}
</Button>
{!isGlobal && (
<InstallAppButton
type={type}
disableInstall={disableInstall}
teamsPlanRequired={teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
...props,
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
};
}
return (
<InstallAppButtonChild appCategories={categories} userAdminTeams={appDbQuery.data?.userAdminTeams} addAppMutationInput={{ type, variant, slug }} multiInstall {...props} />
);
}}
/>
)}
</div>
) : showDisconnectIntegration ? (
<DisconnectIntegration
buttonProps={{ color: "secondary" }}
label={t("disconnect")}
credentialId={existingCredentials[0]}
onSuccess={() => {
appDbQuery.refetch();
}}
/>
) : (
<InstallAppButton
type={type}
disableInstall={disableInstall}
teamsPlanRequired={teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
...props,
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
};
}
return (
<InstallAppButtonChild appCategories={categories} userAdminTeams={appDbQuery.data?.userAdminTeams} addAppMutationInput={{type, variant, slug}} credentials={appDbQuery.data?.credentials} {...props} />
);
}}
/>
))
) : (
<SkeletonButton className="h-10 w-24" />
)}
{dependencies &&
(!dependencyData.isLoading ? (
<div className="mt-6">
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
</div>
) : (
<SkeletonButton className="mt-6 h-20 grow" />
))}
{price !== 0 && (
<span className="block text-right">
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
{feeType === "monthly" && "/" + t("month")}
</span>
)}
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
{body}
</div>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
<span className="text-default">
{teamsPlanRequired ? (
t("teams_plan_required")
) : price === 0 ? (
t("free_to_use_apps")
) : (
<>
{Intl.NumberFormat(i18n.language, {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price)}
{feeType === "monthly" && "/" + t("month")}
</>
)}
</span>
<h4 className="text-emphasis mb-2 mt-8 font-semibold ">{t("contact")}</h4>
<ul className="prose-sm -ml-1 -mr-1 leading-5">
{docs && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis text-sm font-normal no-underline hover:underline"
href={docs}>
<BookOpen className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
)}
{website && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={website}>
<ExternalLink className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
)}
{email && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={"mailto:" + email}>
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{email}
</a>
</li>
)}
{tos && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={tos}>
<File className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
)}
{privacy && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={privacy}>
<Shield className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
)}
</ul>
<hr className="border-subtle my-8 border" />
<span className="leading-1 text-subtle block text-xs">
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
<Flag className="inline h-3 w-3" /> {t("report_app")}
</a>
</div>
</div>
);
};
import type { AppPageProps } from "./AppPage";
import { AppPage } from "./AppPage";
const ShellHeading = () => {
const { t } = useLocale();
return <span className="block py-2">{t("app_store")}</span>;
};
export default function App(props: {
name: string;
description: AppType["description"];
type: AppType["type"];
isGlobal?: AppType["isGlobal"];
logo: string;
slug: string;
variant: string;
body: React.ReactNode;
categories: string[];
author: string;
pro?: boolean;
price?: number;
commission?: number;
feeType?: AppType["feeType"];
docs?: string;
website?: string;
email: string; // required
tos?: string;
privacy?: string;
licenseRequired: AppType["licenseRequired"];
teamsPlanRequired: AppType["teamsPlanRequired"];
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
isTemplate?: boolean;
disableInstall?: boolean;
dependencies?: string[];
}) {
export default function WrappedApp(props: AppPageProps) {
return (
<Shell smallHeading isPublic hideHeadingOnMobile heading={<ShellHeading />} backPath="/apps" withoutSeo>
<HeadSeo
@@ -380,115 +21,11 @@ export default function App(props: {
/>
{props.licenseRequired ? (
<LicenseRequired>
<Component {...props} />
<AppPage {...props} />
</LicenseRequired>
) : (
<Component {...props} />
<AppPage {...props} />
)}
</Shell>
);
}
const InstallAppButtonChild = ({
userAdminTeams,
addAppMutationInput,
appCategories,
multiInstall,
credentials,
...props
}: {
userAdminTeams?: UserAdminTeams;
addAppMutationInput: { type: App["type"]; variant: string; slug: string };
appCategories: string[];
multiInstall?: boolean;
credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"];
} & ButtonProps) => {
const { t } = useLocale();
const router = useRouter();
const mutation = useAddAppMutation(null, {
onSuccess: (data) => {
if (data?.setupPending) return;
showToast(t("app_successfully_installed"), "success");
},
onError: (error) => {
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
},
});
if (!userAdminTeams?.length || appCategories.some((category) => category === "calendar")) {
return <Button
data-testid="install-app-button"
{...props}
// @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now.
color="primary"
size="base">
{multiInstall ? t("install_another") : t("install_app")}
</Button>
}
return (
<Dropdown>
<DropdownMenuTrigger asChild>
<Button
data-testid="install-app-button"
{...props}
// @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now.
color="primary"
size="base">
{multiInstall ? t("install_another") : t("install_app")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
onInteractOutside={(event) => {
if (mutation.isLoading) event.preventDefault();
}}
>
{mutation.isLoading && (
<div className="z-1 fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<Spinner />
</div>
)}
<DropdownMenuLabel>{t("install_app_on")}</DropdownMenuLabel>
{userAdminTeams.map((team) => {
const isInstalled = credentials &&
credentials.some((credential) =>
credential?.teamId ? credential?.teamId === team.id : credential.userId === team.id
)
return (
<DropdownItem
type="button"
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
key={team.id}
disabled={
isInstalled
}
StartIcon={(props) => (
<Avatar
alt={team.logo || ""}
imageSrc={team.logo || `${CAL_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
size="sm"
{...props}
/>
)}
onClick={() => {
mutation.mutate(
team.isUser ? addAppMutationInput : { ...addAppMutationInput, teamId: team.id }
);
}}>
<p>{team.name}{" "}
{isInstalled &&
`(${t("installed")})`}</p>
</DropdownItem>
)})}
</DropdownMenuContent>
</DropdownMenuPortal>
</Dropdown>
);
};
+367
View File
@@ -0,0 +1,367 @@
import Link from "next/link";
import type { IframeHTMLAttributes } from "react";
import React, { useState } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import classNames from "@calcom/lib/classNames";
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { App as AppType } from "@calcom/types/App";
import { Badge, Button, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Shield } from "@calcom/ui/components/icon";
import { InstallAppButtonChild } from "./InstallAppButtonChild";
export type AppPageProps = {
name: string;
description: AppType["description"];
type: AppType["type"];
isGlobal?: AppType["isGlobal"];
logo: string;
slug: string;
variant: string;
body: React.ReactNode;
categories: string[];
author: string;
pro?: boolean;
price?: number;
commission?: number;
feeType?: AppType["feeType"];
docs?: string;
website?: string;
email: string; // required
tos?: string;
privacy?: string;
licenseRequired: AppType["licenseRequired"];
teamsPlanRequired: AppType["teamsPlanRequired"];
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
isTemplate?: boolean;
disableInstall?: boolean;
dependencies?: string[];
concurrentMeetings: AppType["concurrentMeetings"];
};
export const AppPage = ({
name,
type,
logo,
slug,
variant,
body,
categories,
author,
price = 0,
commission,
isGlobal = false,
feeType,
docs,
website,
email,
tos,
privacy,
teamsPlanRequired,
descriptionItems,
isTemplate,
dependencies,
concurrentMeetings,
}: AppPageProps) => {
const { t, i18n } = useLocale();
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
const mutation = useAddAppMutation(null, {
onSuccess: (data) => {
if (data?.setupPending) return;
showToast(t("app_successfully_installed"), "success");
},
onError: (error) => {
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
},
});
const priceInDollar = Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price);
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
const [showDisconnectIntegration, setShowDisconnectIntegration] = useState(false);
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery(
{ appType: type },
{
onSettled(data) {
const credentialsCount = data?.credentials.length || 0;
setShowDisconnectIntegration(
data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
);
setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
},
}
);
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
enabled: !!dependencies,
});
const disableInstall =
dependencyData.data && dependencyData.data.some((dependency) => !dependency.installed);
// const disableInstall = requiresGCal && !gCalInstalled.data;
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
// Such apps, can only be installed once.
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
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">
{hasDescriptionItems && (
<div className="align-center bg-subtle -ml-4 -mr-4 mb-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap p-4 md:-ml-8 md:-mr-8 md:mb-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
{descriptionItems ? (
descriptionItems.map((descriptionItem, index) =>
typeof descriptionItem === "object" ? (
<div
key={`iframe-${index}`}
className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full">
<iframe allowFullScreen {...descriptionItem.iframe} />
</div>
) : (
<img
key={descriptionItem}
src={descriptionItem}
alt={`Screenshot of app ${name}`}
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
/>
)
)
) : (
<SkeletonText />
)}
</div>
)}
<div
className={classNames(
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
hasDescriptionItems && "lg:ml-8"
)}>
<div className="mb-8 flex pt-4">
<header>
<div className="mb-4 flex items-center">
<img
className={classNames(logo.includes("-dark") && "dark:invert", "min-h-16 min-w-16 h-16 w-16")}
src={logo}
alt={name}
/>
<h1 className="font-cal text-emphasis ml-4 text-3xl">{name}</h1>
</div>
<h2 className="text-default text-sm font-medium">
<Link
href={`categories/${categories[0]}`}
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
{categories[0]}
</Link>{" "}
•{" "}
<a target="_blank" rel="noreferrer" href={website}>
{t("published_by", { author })}
</a>
</h2>
{isTemplate && (
<Badge variant="red" className="mt-4">
Template - Available in Dev Environment only for testing
</Badge>
)}
</header>
</div>
{!appDbQuery.isLoading ? (
isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3">
<Button StartIcon={Check} color="secondary" disabled>
{existingCredentials.length > 0
? t("active_install", { count: existingCredentials.length })
: t("default")}
</Button>
{!isGlobal && (
<InstallAppButton
type={type}
disableInstall={disableInstall}
teamsPlanRequired={teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
...props,
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
};
}
return (
<InstallAppButtonChild
appCategories={categories}
userAdminTeams={appDbQuery.data?.userAdminTeams}
addAppMutationInput={{ type, variant, slug }}
multiInstall
concurrentMeetings={concurrentMeetings}
{...props}
/>
);
}}
/>
)}
</div>
) : showDisconnectIntegration ? (
<DisconnectIntegration
buttonProps={{ color: "secondary" }}
label={t("disconnect")}
credentialId={existingCredentials[0]}
onSuccess={() => {
appDbQuery.refetch();
}}
/>
) : (
<InstallAppButton
type={type}
disableInstall={disableInstall}
teamsPlanRequired={teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
...props,
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
};
}
return (
<InstallAppButtonChild
appCategories={categories}
userAdminTeams={appDbQuery.data?.userAdminTeams}
addAppMutationInput={{ type, variant, slug }}
credentials={appDbQuery.data?.credentials}
concurrentMeetings={concurrentMeetings}
{...props}
/>
);
}}
/>
))
) : (
<SkeletonButton className="h-10 w-24" />
)}
{dependencies &&
(!dependencyData.isLoading ? (
<div className="mt-6">
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
</div>
) : (
<SkeletonButton className="mt-6 h-20 grow" />
))}
{price !== 0 && (
<span className="block text-right">
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
{feeType === "monthly" && "/" + t("month")}
</span>
)}
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
{body}
</div>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
<span className="text-default">
{teamsPlanRequired ? (
t("teams_plan_required")
) : price === 0 ? (
t("free_to_use_apps")
) : (
<>
{Intl.NumberFormat(i18n.language, {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price)}
{feeType === "monthly" && "/" + t("month")}
</>
)}
</span>
<h4 className="text-emphasis mb-2 mt-8 font-semibold ">{t("contact")}</h4>
<ul className="prose-sm -ml-1 -mr-1 leading-5">
{docs && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis text-sm font-normal no-underline hover:underline"
href={docs}>
<BookOpen className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
)}
{website && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={website}>
<ExternalLink className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
)}
{email && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={"mailto:" + email}>
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{email}
</a>
</li>
)}
{tos && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={tos}>
<File className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
)}
{privacy && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={privacy}>
<Shield className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
)}
</ul>
<hr className="border-subtle my-8 border" />
<span className="leading-1 text-subtle block text-xs">
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
<Flag className="inline h-3 w-3" /> {t("report_app")}
</a>
</div>
</div>
);
};
@@ -0,0 +1,124 @@
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { doesAppSupportTeamInstall } from "@calcom/app-store/utils";
import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner";
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { AppFrontendPayload } from "@calcom/types/App";
import type { ButtonProps } from "@calcom/ui";
import {
Avatar,
Button,
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuTrigger,
showToast,
} from "@calcom/ui";
export const InstallAppButtonChild = ({
userAdminTeams,
addAppMutationInput,
appCategories,
multiInstall,
credentials,
concurrentMeetings,
...props
}: {
userAdminTeams?: UserAdminTeams;
addAppMutationInput: { type: AppFrontendPayload["type"]; variant: string; slug: string };
appCategories: string[];
multiInstall?: boolean;
credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"];
concurrentMeetings?: boolean;
} & ButtonProps) => {
const { t } = useLocale();
const mutation = useAddAppMutation(null, {
onSuccess: (data) => {
if (data?.setupPending) return;
showToast(t("app_successfully_installed"), "success");
},
onError: (error) => {
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
},
});
if (!userAdminTeams?.length || !doesAppSupportTeamInstall(appCategories, concurrentMeetings)) {
return (
<Button
data-testid="install-app-button"
{...props}
// @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now.
color="primary"
size="base">
{multiInstall ? t("install_another") : t("install_app")}
</Button>
);
}
return (
<Dropdown>
<DropdownMenuTrigger asChild>
<Button
data-testid="install-app-button"
{...props}
// @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now.
color="primary"
size="base">
{multiInstall ? t("install_another") : t("install_app")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
onInteractOutside={(event) => {
if (mutation.isLoading) event.preventDefault();
}}>
{mutation.isLoading && (
<div className="z-1 fixed inset-0 flex items-center justify-center">
<Spinner />
</div>
)}
<DropdownMenuLabel>{t("install_app_on")}</DropdownMenuLabel>
{userAdminTeams.map((team) => {
const isInstalled =
credentials &&
credentials.some((credential) =>
credential?.teamId ? credential?.teamId === team.id : credential.userId === team.id
);
return (
<DropdownItem
type="button"
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
key={team.id}
disabled={isInstalled}
StartIcon={(props) => (
<Avatar
alt={team.logo || ""}
imageSrc={team.logo || `${CAL_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
size="sm"
{...props}
/>
)}
onClick={() => {
mutation.mutate(
team.isUser ? addAppMutationInput : { ...addAppMutationInput, teamId: team.id }
);
}}>
<p>
{team.name} {isInstalled && `(${t("installed")})`}
</p>
</DropdownItem>
);
})}
</DropdownMenuContent>
</DropdownMenuPortal>
</Dropdown>
);
};
@@ -1,9 +1,9 @@
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import type { ComponentProps } from "react";
import React from "react";
import Shell from "@calcom/features/shell/Shell";
import { ShellMain } from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen } from "@calcom/ui";
import { AlertCircle } from "@calcom/ui/components/icon";
@@ -12,7 +12,7 @@ type AppsLayoutProps = {
children: React.ReactNode;
actions?: (className?: string) => JSX.Element;
emptyStore?: boolean;
} & Omit<ComponentProps<typeof Shell>, "actions">;
} & Omit<ComponentProps<typeof ShellMain>, "actions">;
export default function AppsLayout({ children, actions, emptyStore, ...rest }: AppsLayoutProps) {
const { t } = useLocale();
@@ -22,7 +22,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
if (session.status === "loading") return <></>;
return (
<Shell {...rest} actions={actions?.("block")} hideHeadingOnMobile>
<ShellMain {...rest} actions={actions?.("block")} hideHeadingOnMobile>
<div className="flex flex-col xl:flex-row">
<main className="w-full">
{emptyStore ? (
@@ -38,7 +38,6 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
)}
</main>
</div>
</Shell>
</ShellMain>
);
}
export const getLayout = (page: React.ReactElement) => <AppsLayout>{page}</AppsLayout>;
@@ -1,5 +1,5 @@
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { usePathname, useRouter } from "next/navigation";
import type { ComponentProps } from "react";
import React, { useEffect } from "react";
@@ -10,9 +10,9 @@ import { ErrorBoundary } from "@calcom/ui";
export default function AdminLayout({
children,
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
const pathname = usePathname();
const session = useSession();
const router = useRouter();
@@ -23,7 +23,7 @@ export default function AdminLayout({
}
}, [session, router]);
const isAppsPage = router.asPath.startsWith("/settings/admin/apps");
const isAppsPage = pathname?.startsWith("/settings/admin/apps");
return (
<SettingsLayout {...rest}>
<div className="divide-subtle mx-auto flex max-w-4xl flex-row divide-y">
@@ -1,228 +0,0 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import Link from "next/link";
import { useRouter } from "next/router";
import type { FC } from "react";
import { useEffect, useState, useCallback } from "react";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { nameOfDay } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
import classNames from "@lib/classNames";
import { timeZone } from "@lib/clock";
type AvailableTimesProps = {
timeFormat: TimeFormat;
onTimeFormatChange: (is24Hour: boolean) => void;
eventTypeId: number;
recurringCount: number | undefined;
eventTypeSlug: string;
date?: Dayjs;
seatsPerTimeSlot?: number | null;
bookingAttendees?: number | null;
slots?: Slot[];
isLoading: boolean;
duration: number;
};
const AvailableTimes: FC<AvailableTimesProps> = ({
slots = [],
isLoading,
date,
eventTypeId,
eventTypeSlug,
recurringCount,
timeFormat,
onTimeFormatChange,
seatsPerTimeSlot,
bookingAttendees,
duration,
}) => {
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
const { t, i18n } = useLocale();
const router = useRouter();
const { rescheduleUid } = router.query;
const [brand, setBrand] = useState("#292929");
useEffect(() => {
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
}, []);
const isMobile = useMediaQuery("(max-width: 768px)");
const ref = useCallback(
(node: HTMLDivElement) => {
if (isMobile) {
node?.scrollIntoView({ behavior: "smooth" });
}
},
[isMobile]
);
const reserveSlot = (slot: Slot) => {
// Prevent double clicking
if (reserveSlotMutation.isLoading || reserveSlotMutation.isSuccess) {
return;
}
reserveSlotMutation.mutate({
slotUtcStartDate: slot.time,
eventTypeId,
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
bookingUid: slot.bookingUid,
});
};
return (
<div ref={slotPickerRef}>
{!!date ? (
<div className="mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
<div className="mb-4 flex items-center text-left text-base">
<div className="mr-4">
<span className="text-emphasis font-semibold">
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span>
<span className="text-subtle">
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {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
ref={ref}
className="scroll-bar scrollbar-track-w-20 relative -mb-4 flex-grow overflow-y-auto sm:block md:h-[364px]">
{slots.length > 0 &&
slots.map((slot) => {
type BookingURL = {
pathname: string;
query: Record<string, string | number | string[] | undefined | TimeFormat>;
};
const bookingUrl: BookingURL = {
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
query: {
...router.query,
date: dayjs.utc(slot.time).tz(timeZone()).format(),
type: eventTypeId,
slug: eventTypeSlug,
timeFormat,
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
},
};
if (rescheduleUid) {
bookingUrl.query.rescheduleUid = rescheduleUid as string;
}
// If event already has an attendee add booking id
if (slot.bookingUid) {
bookingUrl.query.bookingUid = slot.bookingUid;
}
let slotFull, notEnoughSeats;
if (slot.attendees && seatsPerTimeSlot) slotFull = slot.attendees >= seatsPerTimeSlot;
if (slot.attendees && bookingAttendees && seatsPerTimeSlot) {
notEnoughSeats = slot.attendees + bookingAttendees > seatsPerTimeSlot;
}
const isHalfFull =
slot.attendees && seatsPerTimeSlot && slot.attendees / seatsPerTimeSlot >= 0.5;
const isNearlyFull =
slot.attendees && seatsPerTimeSlot && slot.attendees / seatsPerTimeSlot >= 0.83;
const colorClass = isNearlyFull
? "text-rose-600"
: isHalfFull
? "text-yellow-500"
: "text-emerald-400";
return (
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
{/* Current there is no way to disable Next.js Links */}
{seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? (
<div
className={classNames(
"text-default bg-default border-subtle mb-2 block rounded-sm border py-2 font-medium opacity-25",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}
data-testid="time"
data-disabled="true">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{notEnoughSeats ? (
<p className="text-sm">{t("not_enough_seats")}</p>
) : slots ? (
<p className="text-sm">{t("booking_full")}</p>
) : null}
</div>
) : (
<Link
href={bookingUrl}
prefetch={false}
className={classNames(
" bg-default dark:bg-muted border-default hover:bg-subtle hover:border-brand-default text-emphasis mb-2 block rounded-md border py-2 text-sm font-medium",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}
onClick={() => reserveSlot(slot)}
data-testid="time"
data-disabled="false">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && (
<p className={`${colorClass} text-sm`}>
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
{seatsPerTimeSlot}{" "}
{t("seats_available", {
count: slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot,
})}
</p>
)}
</Link>
)}
</div>
);
})}
{!isLoading && !slots.length && (
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
<h1 className="text-emphasis my-6 text-xl">{t("all_booked_today")}</h1>
</div>
)}
{isLoading && !slots.length && (
<>
<SkeletonContainer className="mb-2">
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
<SkeletonContainer className="mb-2">
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
<SkeletonContainer className="mb-2">
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
</>
)}
</div>
</div>
) : null}
</div>
);
};
AvailableTimes.displayName = "AvailableTimes";
export default AvailableTimes;
@@ -1,38 +0,0 @@
import { i18n } from "next-i18next";
import type { TFunction } from "next-i18next";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { CreditCard } from "@calcom/ui/components/icon";
const BookingDescriptionPayment = (props: {
eventType: Parameters<typeof getPaymentAppData>[0];
t: TFunction;
i18n: typeof i18n;
}) => {
const paymentAppData = getPaymentAppData(props.eventType);
if (!paymentAppData || paymentAppData.price <= 0) return null;
const params = {
amount: paymentAppData.price / 100.0,
formatParams: { amount: { currency: paymentAppData.currency } },
};
return (
<p className="text-bookinglight -ml-2 px-2 text-sm ">
<CreditCard className="-mt-1 ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{paymentAppData.paymentOption === "HOLD" ? (
<>{props.t("no_show_fee_amount", params)}</>
) : (
<>
{/* If undefined this will default to the browser locale */}
{new Intl.NumberFormat(i18n?.language, {
style: "currency",
currency: paymentAppData.currency,
}).format(paymentAppData.price / 100)}
</>
)}
</p>
);
};
export default BookingDescriptionPayment;
+14 -10
View File
@@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState } from "react";
import type { EventLocationType } from "@calcom/app-store/locations";
@@ -158,7 +158,7 @@ function BookingListItem(booking: BookingItemProps) {
id: "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. */
cancel all remaining bookings or just that booking instance. */
href: `/booking/${booking.uid}?cancel=true${
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
@@ -239,13 +239,18 @@ function BookingListItem(booking: BookingItemProps) {
},
});
const saveLocation = (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => {
const saveLocation = (
newLocationType: EventLocationType["type"],
details: {
[key: string]: string;
}
) => {
let newLocation = newLocationType as string;
const eventLocationType = getEventLocationType(newLocationType);
if (eventLocationType?.organizerInputType) {
newLocation = details[Object.keys(details)[0]];
}
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
setLocationMutation.mutate({ bookingId: booking.id, newLocation, details });
};
// Getting accepted recurring dates to show
@@ -255,13 +260,11 @@ function BookingListItem(booking: BookingItemProps) {
.sort((date1: Date, date2: Date) => date1.getTime() - date2.getTime());
const onClickTableData = () => {
router.push({
pathname: `/booking/${booking.uid}`,
query: {
allRemainingBookings: isTabRecurring,
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
},
const urlSearchParams = new URLSearchParams({
allRemainingBookings: isTabRecurring.toString(),
});
if (booking.attendees[0]) urlSearchParams.set("email", booking.attendees[0].email);
router.push(`/booking/${booking.uid}?${urlSearchParams.toString()}`);
};
const title = booking.title;
@@ -283,6 +286,7 @@ function BookingListItem(booking: BookingItemProps) {
saveLocation={saveLocation}
isOpenDialog={isOpenSetLocationDialog}
setShowLocationModal={setIsOpenLocationDialog}
teamId={booking.eventType?.team?.id}
/>
{booking.paid && booking.payment[0] && (
<ChargeCardDialog
@@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -26,6 +26,9 @@ type Props = {
};
export default function CancelBooking(props: Props) {
const pathname = usePathname();
const searchParams = useSearchParams();
const asPath = `${pathname}?${searchParams.toString()}`;
const [cancellationReason, setCancellationReason] = useState<string>("");
const { t } = useLocale();
const router = useRouter();
@@ -97,7 +100,7 @@ export default function CancelBooking(props: Props) {
});
if (res.status >= 200 && res.status < 300) {
await router.replace(router.asPath);
router.replace(asPath);
} else {
setLoading(false);
setError(
-198
View File
@@ -1,198 +0,0 @@
import type { EventType } from "@prisma/client";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import type { z } from "zod";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import DatePicker from "@calcom/features/calendars/DatePicker";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { TimeFormat } from "@calcom/lib/timeFormat";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import useRouterQuery from "@lib/hooks/useRouterQuery";
const AvailableTimes = dynamic(() => import("@components/booking/AvailableTimes"));
const getRefetchInterval = (refetchCount: number): number => {
const intervals = [3000, 3000, 5000, 10000, 20000, 30000] as const;
return intervals[refetchCount] || intervals[intervals.length - 1];
};
const useSlots = ({
eventTypeId,
eventTypeSlug,
startTime,
endTime,
usernameList,
timeZone,
duration,
enabled = true,
}: {
eventTypeId: number;
eventTypeSlug: string;
startTime?: Dayjs;
endTime?: Dayjs;
usernameList: string[];
timeZone?: string;
duration?: string;
enabled?: boolean;
}) => {
const [refetchCount, setRefetchCount] = useState(0);
const refetchInterval = getRefetchInterval(refetchCount);
const { data, isLoading, isPaused, fetchStatus } = trpc.viewer.public.slots.getSchedule.useQuery(
{
eventTypeId,
eventTypeSlug,
usernameList,
startTime: startTime?.toISOString() || "",
endTime: endTime?.toISOString() || "",
timeZone,
duration,
},
{
enabled: !!startTime && !!endTime && enabled,
refetchInterval,
trpc: { context: { skipBatch: true } },
}
);
useEffect(() => {
if (!!data && fetchStatus === "idle") {
setRefetchCount(refetchCount + 1);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetchStatus, data]);
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
return { slots: data?.slots || {}, isLoading: isLoading || isPaused };
};
export const SlotPicker = ({
eventType,
timeFormat,
onTimeFormatChange,
timeZone,
recurringEventCount,
users,
seatsPerTimeSlot,
bookingAttendees,
weekStart = 0,
}: {
eventType: Pick<
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
"id" | "schedulingType" | "slug" | "length" | "metadata"
>;
timeFormat: TimeFormat;
onTimeFormatChange: (is24Hour: boolean) => void;
timeZone?: string;
seatsPerTimeSlot?: number;
bookingAttendees?: number;
recurringEventCount?: number;
users: string[];
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
}) => {
const [selectedDate, setSelectedDate] = useState<Dayjs>();
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
let { duration = eventType.length.toString() } = useRouterQuery("duration");
const { date, setQuery: setDate } = useRouterQuery("date");
const { month, setQuery: setMonth } = useRouterQuery("month");
const router = useRouter();
if (!eventType.metadata?.multipleDuration) {
duration = eventType.length.toString();
}
useEffect(() => {
if (!router.isReady) return;
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
if (timeZone === "Etc/GMT") {
setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0));
if (date) {
setSelectedDate(dayjs.utc(date));
}
} else {
// Set the start of the month without shifting time like startOf() may do.
setBrowsingDate(
dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)
);
if (date) {
// It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime.
setSelectedDate(dayjs.tz(date, timeZone));
}
}
}, [router.isReady, month, date, duration, timeZone]);
const { i18n, isLocaleReady } = useLocale();
const { slots: monthSlots, isLoading } = useSlots({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
startTime: dayjs().startOf("day"),
endTime: browsingDate?.endOf("month"),
timeZone,
duration,
});
const { slots: selectedDateSlots, isLoading: _isLoadingSelectedDateSlots } = useSlots({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
startTime: selectedDate?.startOf("day"),
endTime: selectedDate?.endOf("day"),
timeZone,
duration,
/** Prevent refetching is we already have this data from month slots */
enabled: !!selectedDate,
});
/** Hide skeleton if we have the slot loaded in the month query */
const isLoadingSelectedDateSlots = (() => {
if (!selectedDate) return _isLoadingSelectedDateSlots;
if (!!selectedDateSlots[selectedDate.format("YYYY-MM-DD")]) return false;
if (!!monthSlots[selectedDate.format("YYYY-MM-DD")]) return false;
return false;
})();
return (
<>
<DatePicker
isLoading={isLoading}
className={classNames(
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]",
selectedDate ? " border-subtle sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
)}
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
locale={isLocaleReady ? i18n.language : "en"}
selected={selectedDate}
onChange={(newDate) => {
setDate(newDate.format("YYYY-MM-DD"));
}}
onMonthChange={(newMonth) => {
setMonth(newMonth.format("YYYY-MM"));
}}
browsingDate={browsingDate}
weekStart={weekStart}
/>
<AvailableTimes
isLoading={isLoadingSelectedDateSlots}
slots={
selectedDate &&
(selectedDateSlots[selectedDate.format("YYYY-MM-DD")] ||
monthSlots[selectedDate.format("YYYY-MM-DD")])
}
date={selectedDate}
timeFormat={timeFormat}
onTimeFormatChange={onTimeFormatChange}
eventTypeId={eventType.id}
eventTypeSlug={eventType.slug}
seatsPerTimeSlot={seatsPerTimeSlot}
bookingAttendees={bookingAttendees}
recurringCount={recurringEventCount}
duration={parseInt(duration)}
/>
</>
);
};
@@ -1,41 +0,0 @@
import type { FC } from "react";
import { useEffect, useState } from "react";
import type { ITimezoneOption } from "@calcom/ui";
import { TimezoneSelect } from "@calcom/ui";
import { timeZone } from "../../lib/clock";
type Props = {
onSelectTimeZone: (selectedTimeZone: string) => void;
};
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
const [selectedTimeZone, setSelectedTimeZone] = useState("");
useEffect(() => {
setSelectedTimeZone(timeZone());
}, []);
useEffect(() => {
if (selectedTimeZone && timeZone() && selectedTimeZone !== timeZone()) {
onSelectTimeZone(timeZone(selectedTimeZone));
}
}, [selectedTimeZone, onSelectTimeZone]);
return !!selectedTimeZone ? (
<TimezoneSelect
id="timeZone"
classNames={{
singleValue: () => "text-default",
dropdownIndicator: () => "text-default",
menu: () => "!w-64 max-w-[90vw] shadow-dropdown bg-default border-subtle border rounded-md mt-1",
}}
variant="minimal"
value={selectedTimeZone}
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
/>
) : null;
};
export default TimeOptions;
@@ -1,26 +0,0 @@
import { Globe } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
import TimeOptions from "@components/booking/TimeOptions";
export function TimezoneDropdown({
onChangeTimeZone,
}: {
onChangeTimeZone: (newTimeZone: string) => void;
timeZone?: string;
}) {
const handleSelectTimeZone = (newTimeZone: string) => {
onChangeTimeZone(newTimeZone);
localStorageTimeZone(newTimeZone);
};
return (
<>
<div className="dark:focus-within:bg-darkgray-200 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
<Globe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</div>
</>
);
}
@@ -1,44 +0,0 @@
import { CAL_URL } from "@calcom/lib/constants";
import type { AvatarGroupProps } from "@calcom/ui";
import { AvatarGroup } from "@calcom/ui";
export const UserAvatars = ({
profile,
users,
...props
}: {
profile: { image: string | null; name?: string | null; username?: string | null };
showMembers: boolean;
users: { username: string | null; name?: string | null }[];
} & Pick<AvatarGroupProps, "size" | "truncateAfter">) => {
const showMembers = !users.find((user) => user.name === profile.name) && props.showMembers;
return (
<AvatarGroup
items={
[
{
image: profile.image,
alt: profile.name,
title: profile.name,
href: profile.username ? `${CAL_URL}/${profile.username}` : undefined,
},
...(showMembers
? users.map((user) => ({
title: user.name,
image: `${CAL_URL}/${user.username}/avatar.png`,
alt: user.name || undefined,
href: user.username ? `${CAL_URL}/${user.username}` : undefined,
}))
: []),
].filter((item) => !!item.image) as {
image: string;
alt?: string;
title?: string;
href?: string;
}[]
}
size="sm"
truncateAfter={props.truncateAfter}
/>
);
};
@@ -104,6 +104,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
phone: z.string().optional().nullable(),
locationAddress: z.string().optional(),
credentialId: z.number().optional(),
teamName: z.string().optional(),
locationLink: z
.string()
.optional()
@@ -298,8 +299,19 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
}
if (values.credentialId) {
details = { ...details, credentialId: values.credentialId };
details = {
...details,
credentialId: values.credentialId,
};
}
if (values.teamName) {
details = {
...details,
teamName: values.teamName,
};
}
saveLocation(newLocation, details);
setShowLocationModal(false);
setSelectedLocation?.(undefined);
@@ -344,6 +356,11 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
onChange={(val) => {
if (val) {
locationFormMethods.setValue("locationType", val.value);
if (val.credential) {
locationFormMethods.setValue("credentialId", val.credential.id);
locationFormMethods.setValue("teamName", val.credential.team?.name);
}
locationFormMethods.unregister([
"locationLink",
"locationAddress",
@@ -5,7 +5,7 @@ import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import { z } from "zod";
import type { z } from "zod";
import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } from "@calcom/core/event";
@@ -17,15 +17,24 @@ import {
allowDisablingHostConfirmationEmails,
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { EditableSchema } from "@calcom/features/form-builder/FormBuilderFieldsSchema"
import type { EditableSchema } from "@calcom/features/form-builder/schema";
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
import { Edit, Copy } from "@calcom/ui/components/icon";
import {
Alert,
Button,
CheckboxField,
Label,
SettingsToggle,
showToast,
TextField,
Tooltip,
} from "@calcom/ui";
import { Copy, Edit } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -87,7 +96,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
return {
...field,
hidden: !enabled,
editable: (!enabled ? "system-but-hidden" : "system-but-optional") as z.infer<typeof EditableSchema>
editable: (!enabled ? "system-but-hidden" : "system-but-optional") as z.infer<
typeof EditableSchema
>,
};
}
return field;
@@ -193,6 +204,21 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
onRequiresConfirmation={setRequiresConfirmation}
/>
<hr className="border-subtle" />
<Controller
name="requiresBookerEmailVerification"
control={formMethods.control}
defaultValue={eventType.requiresBookerEmailVerification}
render={({ field: { value, onChange } }) => (
<SettingsToggle
title={t("requires_booker_email_verification")}
{...shouldLockDisableProps("requiresBookerEmailVerification")}
description={t("description_requires_booker_email_verification")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
)}
/>
<hr className="border-subtle" />
<Controller
name="hideCalendarNotes"
control={formMethods.control}
@@ -340,7 +366,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
}}
/>
<div className="mt-2">
<Checkbox
<CheckboxField
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
+16 -2
View File
@@ -1,3 +1,5 @@
import { Trans } from "next-i18next";
import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useFormContext } from "react-hook-form";
@@ -158,9 +160,21 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
{!shouldLockDisableProps("apps").disabled && (
<div className="bg-muted rounded-md p-8">
{!isLoading && notInstalledApps?.length ? (
<h2 className="text-emphasis text-lg font-semibold">{t("available_apps")}</h2>
<>
<h2 className="text-emphasis mb-2 text-xl font-semibold leading-5 tracking-[0.01em]">
{t("available_apps_lower_case")}
</h2>
<p className="text-default mb-6 text-sm font-normal">
<Trans i18nKey="available_apps_desc">
You have no apps installed. View popular apps below and explore more in our &nbsp;
<Link className="cursor-pointer underline" href="/apps">
App Store
</Link>
</Trans>
</p>
</>
) : null}
<div className="before:border-0">
<div className="bg-default border-subtle divide-subtle divide-y rounded-md border before:border-0">
{notInstalledApps?.map((app) => (
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
@@ -1,5 +1,5 @@
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { useState, memo } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type { OptionProps, SingleValueProps } from "react-select";
import { components } from "react-select";
@@ -71,86 +71,91 @@ const format = (date: Date, hour12: boolean) =>
hourCycle: hour12 ? "h12" : "h24",
}).format(new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")));
const EventTypeScheduleDetails = ({
isManagedEventType,
selectedScheduleValue,
}: {
isManagedEventType: boolean;
selectedScheduleValue: AvailabilityOption | undefined;
}) => {
const { data: loggedInUser } = useMeQuery();
const timeFormat = loggedInUser?.timeFormat;
const { t, i18n } = useLocale();
const { watch } = useFormContext<FormValues>();
const EventTypeScheduleDetails = memo(
({
isManagedEventType,
selectedScheduleValue,
}: {
isManagedEventType: boolean;
selectedScheduleValue: AvailabilityOption | undefined;
}) => {
const { data: loggedInUser } = useMeQuery();
const timeFormat = loggedInUser?.timeFormat;
const { t, i18n } = useLocale();
const { watch } = useFormContext<FormValues>();
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{
scheduleId: scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
isManagedEventType,
},
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
);
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{
scheduleId:
scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
isManagedEventType,
},
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
);
const filterDays = (dayNum: number) =>
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
const filterDays = (dayNum: number) =>
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
return (
<div className="border-default space-y-4 rounded border px-6 pb-4">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
return (
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span
className={classNames(
"w-20 font-medium sm:w-32 ",
!isAvailable ? "text-subtle line-through" : "text-default"
)}>
{day}
</span>
{isLoading ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="text-default flex items-center leading-4">
<span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ms-4">-</span>
<div className="ml-6 sm:w-28">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
</div>
) : (
<span className="text-subtle ml-6 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr className="border-subtle" />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
{!!schedule?.id && !schedule.isManaged && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
color="minimal"
EndIcon={ExternalLink}
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
)}
return (
<div className="border-default space-y-4 rounded border px-6 pb-4">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
return (
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span
className={classNames(
"w-20 font-medium sm:w-32 ",
!isAvailable ? "text-subtle line-through" : "text-default"
)}>
{day}
</span>
{isLoading ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="text-default flex items-center leading-4">
<span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ms-4">-</span>
<div className="ml-6 sm:w-28">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
</div>
) : (
<span className="text-subtle ml-6 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr className="border-subtle" />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
{!!schedule?.id && !schedule.isManaged && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
color="minimal"
EndIcon={ExternalLink}
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
)}
</div>
</div>
</div>
);
};
);
}
);
EventTypeScheduleDetails.displayName = "EventTypeScheduleDetails";
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
@@ -13,6 +13,7 @@ import type { DurationType } from "@calcom/lib/convertToNewDurationType";
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
import findDurationType from "@calcom/lib/findDurationType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ascendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimit";
import type { PeriodType } from "@calcom/prisma/enums";
import type { IntervalLimit } from "@calcom/types/Calendar";
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
@@ -462,7 +463,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
type="number"
{...offsetStartLockedProps}
label={t("offset_start")}
defaultValue={eventType.offsetStart}
{...formMethods.register("offsetStart")}
addOnSuffix={<>{t("minutes")}</>}
hint={t("offset_start_description", {
@@ -477,11 +477,9 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
type IntervalLimitsKey = keyof IntervalLimit;
const intervalOrderKeys = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"] as const;
const INTERVAL_LIMIT_OPTIONS = intervalOrderKeys.map((key) => ({
const INTERVAL_LIMIT_OPTIONS = ascendingLimitKeys.map((key) => ({
value: key as keyof IntervalLimit,
label: `Per ${key.split("_")[1].toLocaleLowerCase()}`,
label: `Per ${intervalLimitKeyToUnit(key)}`,
}));
type IntervalLimitItemProps = {
@@ -590,8 +588,8 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
Object.entries(currentIntervalLimits)
.sort(([limitKeyA], [limitKeyB]) => {
return (
intervalOrderKeys.indexOf(limitKeyA as IntervalLimitsKey) -
intervalOrderKeys.indexOf(limitKeyB as IntervalLimitsKey)
ascendingLimitKeys.indexOf(limitKeyA as IntervalLimitsKey) -
ascendingLimitKeys.indexOf(limitKeyB as IntervalLimitsKey)
);
})
.map(([key, value]) => {
@@ -306,7 +306,9 @@ export const EventSetupTab = (
)}
alt={`${eventLocationType.label} logo`}
/>
<span className="ms-1 line-clamp-1 text-sm">{eventLabel}</span>
<span className="ms-1 line-clamp-1 text-sm">{`${eventLabel} ${
location.teamName ? `(${location.teamName})` : ""
}`}</span>
</div>
<div className="flex">
<button
+31 -7
View File
@@ -1,3 +1,5 @@
import { Trans } from "next-i18next";
import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef } from "react";
import type { ComponentProps } from "react";
@@ -19,16 +21,17 @@ interface IUserToValue {
email: string;
}
const mapUserToValue = ({ id, name, username, email }: IUserToValue) => ({
const mapUserToValue = ({ id, name, username, email }: IUserToValue, pendingString: string) => ({
value: `${id || ""}`,
label: `${name || ""}`,
label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`,
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
email,
});
export const mapMemberToChildrenOption = (
member: EventTypeSetupProps["teamMembers"][number],
slug: string
slug: string,
pendingString: string
) => {
return {
slug,
@@ -43,7 +46,7 @@ export const mapMemberToChildrenOption = (
eventTypeSlugs: member.eventTypes ?? [],
},
value: `${member.id ?? ""}`,
label: member.name ?? "",
label: `${member.name || member.email || ""}${!member.username ? ` (${pendingString})` : ""}`,
};
};
@@ -98,6 +101,7 @@ const CheckedHostField = ({
isFixed,
value,
onChange,
helperText,
...rest
}: {
labelText: string;
@@ -106,6 +110,7 @@ const CheckedHostField = ({
value: { isFixed: boolean; userId: number }[];
onChange?: (options: { isFixed: boolean; userId: number }[]) => void;
options?: Options<CheckedSelectOption>;
helperText?: React.ReactNode | string;
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
return (
<div className="bg-muted flex flex-col space-y-5 p-4">
@@ -135,11 +140,24 @@ const CheckedHostField = ({
placeholder={placeholder}
{...rest}
/>
{helperText && <p className="text-subtle text-sm">{helperText}</p>}
</div>
</div>
);
};
const FixedHostHelper = (
<Trans i18nKey="fixed_host_helper">
Add anyone who needs to attend the event.
<Link
className="underline underline-offset-2"
target="_blank"
href="https://cal.com/docs/enterprise-features/teams/round-robin-scheduling#fixed-hosts">
Learn more
</Link>
</Trans>
);
const RoundRobinHosts = ({
teamMembers,
value,
@@ -166,6 +184,7 @@ const RoundRobinHosts = ({
value={value}
placeholder={t("add_fixed_hosts")}
labelText={t("fixed_hosts")}
helperText={FixedHostHelper}
/>
<CheckedHostField
options={teamMembers.sort(sortByLabel)}
@@ -174,6 +193,7 @@ const RoundRobinHosts = ({
isFixed={false}
placeholder={t("add_attendees")}
labelText={t("round_robin_hosts")}
helperText={t("round_robin_helper")}
/>
</>
);
@@ -291,9 +311,13 @@ export const EventTeamTab = ({
// description: t("round_robin_description"),
},
];
const teamMembersOptions = teamMembers.map(mapUserToValue);
const childrenEventTypeOptions = teamMembers.map((member) => {
return mapMemberToChildrenOption(member, eventType.slug);
const pendingMembers = (member: (typeof teamMembers)[number]) =>
!!eventType.team?.parentId || !!member.username;
const teamMembersOptions = teamMembers
.filter(pendingMembers)
.map((member) => mapUserToValue(member, t("pending")));
const childrenEventTypeOptions = teamMembers.filter(pendingMembers).map((member) => {
return mapMemberToChildrenOption(member, eventType.slug, t("pending"));
});
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
return (
@@ -1,13 +1,14 @@
import { Webhook as TbWebhook } from "lucide-react";
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useState, Suspense } from "react";
import type { UseFormReturn } from "react-hook-form";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
@@ -51,7 +52,6 @@ import {
Loader,
} from "@calcom/ui/components/icon";
import { EmbedButton, EmbedDialog } from "@components/Embed";
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
type Props = {
@@ -148,7 +148,7 @@ function EventTypeSingleLayout({
onSuccess: async () => {
await utils.viewer.eventTypes.invalidate();
showToast(t("event_type_deleted_successfully"), "success");
await router.push("/event-types");
router.push("/event-types");
setDeleteDialogOpen(false);
},
onError: (err) => {
@@ -209,10 +209,8 @@ function EventTypeSingleLayout({
}`,
});
}
if (isManagedEventType || isChildrenManagedEventType) {
// Removing apps and workflows for manageg event types by admins v1
navigation.splice(0, 1);
} else {
const showWebhooks = !(isManagedEventType || isChildrenManagedEventType);
if (showWebhooks) {
navigation.push({
name: "webhooks",
href: `/event-types/${eventType.id}?tabName=webhooks`,
@@ -234,7 +232,7 @@ function EventTypeSingleLayout({
formMethods,
]);
const orgBranding = useOrgBrandingValues();
const orgBranding = useOrgBranding();
const isOrgEvent = orgBranding?.fullDomain;
const permalink = `${orgBranding?.fullDomain ?? CAL_URL}/${
team ? `${!isOrgEvent ? "team/" : ""}${team.slug}` : eventType.users[0].username
@@ -312,12 +310,13 @@ function EventTypeSingleLayout({
showToast("Link copied!", "success");
}}
/>
<EmbedButton
<EventTypeEmbedButton
embedUrl={encodeURIComponent(embedLink)}
StartIcon={Code}
color="secondary"
variant="icon"
tooltip={t("embed")}
eventId={eventType.id}
/>
</>
)}
@@ -407,13 +406,13 @@ function EventTypeSingleLayout({
className="primary-navigation"
tabs={EventTypeTabs}
sticky
linkProps={{ shallow: true }}
linkShallow
itemClassname="items-start"
iconClassName="md:mt-px"
/>
</div>
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
<HorizontalTabs tabs={EventTypeTabs} linkProps={{ shallow: true }} />
<HorizontalTabs tabs={EventTypeTabs} linkShallow />
</div>
<div className="w-full ltr:mr-2 rtl:ml-2">
<div
@@ -451,7 +450,7 @@ function EventTypeSingleLayout({
</p>
</ConfirmationDialogContent>
</Dialog>
<EmbedDialog />
<EventTypeEmbedDialog />
</Shell>
);
}
@@ -1,4 +1,3 @@
import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { Schedule } from "@calcom/features/schedules";
@@ -21,12 +20,10 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const { t } = useLocale();
const { nextStep } = props;
const router = useRouter();
const queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: defaultScheduleId! },
{
enabled: router.isReady && !!defaultScheduleId,
enabled: !!defaultScheduleId,
}
);
@@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import type { FormEvent } from "react";
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
@@ -15,6 +15,7 @@ import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
interface IUserSettingsProps {
nextStep: () => void;
hideUsername?: boolean;
}
const UserSettings = (props: IUserSettingsProps) => {
@@ -66,8 +67,8 @@ const UserSettings = (props: IUserSettingsProps) => {
return (
<form onSubmit={onSubmit}>
<div className="space-y-6">
{/* Username textfield */}
<UsernameAvailabilityField />
{/* Username textfield: when not coming from signup */}
{!props.hideUsername && <UsernameAvailabilityField />}
{/* Full name textfield */}
<div className="w-full">
@@ -12,7 +12,7 @@ import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
interface DisableTwoFactorAuthModalProps {
open: boolean;
onOpenChange: () => void;
disablePassword?: boolean;
/** Called when the user closes the modal without disabling two-factor auth */
onCancel: () => void;
/** Called when the user disables two-factor auth */
@@ -27,6 +27,7 @@ interface DisableTwoFactorValues {
const DisableTwoFactorAuthModal = ({
onDisable,
onCancel,
disablePassword,
open,
onOpenChange,
}: DisableTwoFactorAuthModalProps) => {
@@ -75,23 +76,30 @@ const DisableTwoFactorAuthModal = ({
<DialogContent title={t("disable_2fa")} description={t("disable_2fa_recommendation")} type="creation">
<Form form={form} handleSubmit={handleDisable}>
<div className="mb-8">
<PasswordField
labelProps={{
className: "block text-sm font-medium text-default",
}}
{...form.register("password")}
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
/>
{!disablePassword && (
<PasswordField
labelProps={{
className: "block text-sm font-medium text-default",
}}
{...form.register("password")}
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
/>
)}
<TwoFactor center={false} />
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
</div>
<DialogFooter showDivider className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<DialogFooter showDivider className="relative mt-5">
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
<Button type="submit" className="me-2 ms-2" data-testid="disable-2fa" disabled={isDisabling}>
<Button
type="submit"
className="me-2 ms-2"
data-testid="disable-2fa"
loading={isDisabling}
disabled={isDisabling}>
{t("disable")}
</Button>
</DialogFooter>
@@ -185,7 +185,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
)}
</div>
</WithStep>
<DialogFooter className="mt-8 sm:flex sm:flex-row-reverse" showDivider>
<DialogFooter className="mt-8" showDivider>
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
@@ -194,6 +194,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
type="submit"
className="me-2 ms-2"
onClick={handleSetup}
loading={isSubmitting}
disabled={password.length === 0 || isSubmitting}>
{t("continue")}
</Button>
@@ -208,7 +209,12 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
</Button>
</WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<Button type="submit" className="me-2 ms-2" data-testid="enable-2fa" disabled={isSubmitting}>
<Button
type="submit"
className="me-2 ms-2"
data-testid="enable-2fa"
loading={isSubmitting}
disabled={isSubmitting}>
{t("enable")}
</Button>
</WithStep>
+1 -1
View File
@@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import type { Dispatch, SetStateAction } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
+4 -5
View File
@@ -1,25 +1,24 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { md } from "@calcom/lib/markdownIt";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { Avatar } from "@calcom/ui";
type TeamType = NonNullable<TeamWithMembers>;
type TeamType = Omit<NonNullable<TeamWithMembers>, "inviteToken">;
type MembersType = TeamType["members"];
type MemberType = MembersType[number] & { safeBio: string | null };
type TeamTypeWithSafeHtml = Omit<TeamType, "members" | "inviteToken"> & { members: MemberType[] };
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
const routerQuery = useRouterQuery();
const { t } = useLocale();
const router = useRouter();
const isBioEmpty = !member.bio || !member.bio.replace("<p><br></p>", "").length;
// We don't want to forward orgSlug and user which are route params to the next route
const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = router.query;
const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = routerQuery;
return (
<Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}>
@@ -1,7 +1,7 @@
import classNames from "classnames";
import { debounce, noop } from "lodash";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import type { RefCallback } from "react";
import { useEffect, useMemo, useState } from "react";
@@ -14,7 +14,7 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, Input, Label } from "@calcom/ui";
import { Check, Edit2, ExternalLink, Star as StarSolid } from "@calcom/ui/components/icon";
export enum UsernameChangeStatusEnum {
@@ -46,6 +46,9 @@ const obtainNewUsernameChangeCondition = ({
};
const PremiumTextfield = (props: ICustomUsernameProps) => {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const { t } = useLocale();
const { data: session, update } = useSession();
const {
@@ -61,8 +64,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
const [user] = trpc.viewer.me.useSuspenseQuery();
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
const [markAsError, setMarkAsError] = useState(false);
const router = useRouter();
const { paymentStatus: recentAttemptPaymentStatus } = router.query;
const recentAttemptPaymentStatus = searchParams?.get("recentAttemptPaymentStatus");
const [openDialogSaveUsername, setOpenDialogSaveUsername] = useState(false);
const { data: stripeCustomer } = trpc.viewer.stripeCustomer.useQuery();
const isCurrentUsernamePremium =
@@ -96,7 +98,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
const updateUsername = trpc.viewer.updateProfile.useMutation({
onSuccess: async () => {
onSuccessMutation && (await onSuccessMutation());
await update();
await update({ username: inputUsernameValue });
setOpenDialogSaveUsername(false);
},
onError: (error) => {
@@ -120,7 +122,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
const paymentLink = `/api/integrations/stripepayment/subscription?intentUsername=${
inputUsernameValue || usernameFromStripe
}&action=${usernameChangeCondition}&callbackUrl=${WEBAPP_URL}${router.asPath}`;
}&action=${usernameChangeCondition}&callbackUrl=${WEBAPP_URL}${pathname}`;
const ActionButtons = () => {
if (paymentRequired) {
@@ -223,7 +225,11 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
onChange={(event) => {
event.preventDefault();
// Reset payment status
delete router.query.paymentStatus;
const _searchParams = new URLSearchParams(searchParams);
_searchParams.delete("paymentStatus");
if (searchParams.toString() !== _searchParams.toString()) {
router.replace(`${pathname}?${_searchParams.toString()}`);
}
setInputUsernameValue(event.target.value);
}}
data-testid="username-input"
@@ -284,7 +290,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
</div>
</div>
<div className="mt-4 flex flex-row-reverse gap-x-2">
<DialogFooter className="mt-4">
{/* redirect to checkout */}
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
<Button
@@ -312,7 +318,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<DialogClose color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
{t("cancel")}
</DialogClose>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
@@ -9,7 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, TextField } from "@calcom/ui";
import { Button, Dialog, DialogClose, DialogContent, TextField, DialogFooter } from "@calcom/ui";
import { Check, Edit2 } from "@calcom/ui/components/icon";
interface ICustomUsernameProps {
@@ -25,6 +25,7 @@ interface ICustomUsernameProps {
const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.ComponentProps<typeof TextField>>) => {
const { t } = useLocale();
const { data: session, update } = useSession();
const {
currentUsername,
setCurrentUsername = noop,
@@ -71,7 +72,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
onSuccessMutation && (await onSuccessMutation());
setOpenDialogSaveUsername(false);
setCurrentUsername(inputUsernameValue);
await update();
await update({ username: inputUsernameValue });
},
onError: (error) => {
onErrorMutation && onErrorMutation(error);
@@ -175,7 +176,8 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
</div>
</div>
</div>
<div className="ml-4 mt-4 flex flex-row-reverse gap-x-2">
<DialogFooter className="mt-4">
<Button
type="button"
loading={updateUsernameMutation.isLoading}
@@ -187,7 +189,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
<DialogClose color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
{t("cancel")}
</DialogClose>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
@@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
@@ -35,12 +35,12 @@ export const UsernameAvailabilityField = ({
onSuccessMutation,
onErrorMutation,
}: UsernameAvailabilityFieldProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const [user] = trpc.viewer.me.useSuspenseQuery();
const [currentUsernameState, setCurrentUsernameState] = useState(user.username || "");
const { username: usernameFromQuery, setQuery: setUsernameFromQuery } = useRouterQuery("username");
const { username: currentUsername, setQuery: setCurrentUsername } =
router.query["username"] && user.username === null
searchParams?.get("username") && user.username === null
? { username: usernameFromQuery, setQuery: setUsernameFromQuery }
: { username: currentUsernameState || "", setQuery: setCurrentUsernameState };
const formMethods = useForm({
@@ -2,6 +2,7 @@ import type { GroupBase, Props, SingleValue } from "react-select";
import { components } from "react-select";
import type { EventLocationType } from "@calcom/app-store/locations";
import type { CredentialDataWithTeamName } from "@calcom/app-store/utils";
import { classNames } from "@calcom/lib";
import cx from "@calcom/lib/classNames";
import { Select } from "@calcom/ui";
@@ -12,6 +13,7 @@ export type LocationOption = {
icon?: string;
disabled?: boolean;
address?: string;
credential?: CredentialDataWithTeamName;
};
export type SingleValueLocationOption = SingleValue<LocationOption>;
+62 -27
View File
@@ -1,16 +1,16 @@
import { TooltipProvider } from "@radix-ui/react-tooltip";
import type { Session } from "next-auth";
import { SessionProvider } from "next-auth/react";
import { useSession } from "next-auth/react";
import { EventCollectionProvider } from "next-collect/client";
import type { SSRConfig } from "next-i18next";
import { appWithTranslation } from "next-i18next";
import { ThemeProvider } from "next-themes";
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
import type { NextRouter } from "next/router";
import { useRouter } from "next/router";
import type { ParsedUrlQuery } from "querystring";
import type { ComponentProps, PropsWithChildren, ReactNode } from "react";
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { FeatureProvider } from "@calcom/features/flags/context/provider";
@@ -18,25 +18,32 @@ import { useFlags } from "@calcom/features/flags/hooks";
import { trpc } from "@calcom/trpc/react";
import { MetaProvider } from "@calcom/ui";
import usePublicPage from "@lib/hooks/usePublicPage";
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
import type { WithNonceProps } from "@lib/withNonce";
import { useViewerI18n } from "@components/I18nLanguageHandler";
const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children: React.ReactNode }>(
({ children }) => <>{children}</>
);
const I18nextAdapter = appWithTranslation<
NextJsAppProps<SSRConfig> & {
children: React.ReactNode;
}
>(({ children }) => <>{children}</>);
// Workaround for https://github.com/vercel/next.js/issues/8592
export type AppProps = Omit<
NextAppProps<WithNonceProps & { themeBasis?: string } & Record<string, unknown>>,
NextAppProps<
WithNonceProps & {
themeBasis?: string;
session: Session;
} & Record<string, unknown>
>,
"Component"
> & {
Component: NextAppProps["Component"] & {
requiresLicense?: boolean;
isThemeSupported?: boolean;
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
isBookingPage?: boolean | ((arg: { router: NextAppProps["router"] }) => boolean);
getLayout?: (page: React.ReactElement, router: NextAppProps["router"]) => ReactNode;
PageWrapper?: (props: AppProps) => JSX.Element;
};
@@ -48,13 +55,18 @@ type AppPropsWithChildren = AppProps & {
children: ReactNode;
};
const getEmbedNamespace = (query: ReturnType<typeof useRouter>["query"]) => {
const getEmbedNamespace = (query: ParsedUrlQuery) => {
// Mostly embed query param should be available on server. Use that there.
// Use the most reliable detection on client
return typeof window !== "undefined" ? window.getEmbedNamespace() : (query.embed as string) || null;
};
const CustomI18nextProvider = (props: AppPropsWithChildren) => {
// We dont need to pass nonce to the i18n provider - this was causing x2-x3 re-renders on a hard refresh
type AppPropsWithoutNonce = Omit<AppPropsWithChildren, "pageProps"> & {
pageProps: Omit<AppPropsWithChildren["pageProps"], "nonce">;
};
const CustomI18nextProvider = (props: AppPropsWithoutNonce) => {
/**
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
* We intend to not cache i18n query
@@ -71,6 +83,7 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
},
router: locale ? { locale } : props.router,
} as unknown as ComponentProps<typeof I18nextAdapter>;
return <I18nextAdapter {...passedProps} />;
};
@@ -84,20 +97,19 @@ const enum ThemeSupport {
}
type CalcomThemeProps = PropsWithChildren<
Pick<AppProps["pageProps"], "nonce" | "themeBasis"> &
Pick<AppProps, "router"> &
Pick<AppProps["pageProps"], "nonce" | "themeBasis"> &
Pick<AppProps["Component"], "isBookingPage" | "isThemeSupported">
>;
const CalcomThemeProvider = (props: CalcomThemeProps) => {
const router = useRouter();
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
// One such example is our Embeds Demo and Testing page at http://localhost:3100
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
const embedNamespace = getEmbedNamespace(router.query);
const embedNamespace = getEmbedNamespace(props.router.query);
const isEmbedMode = typeof embedNamespace === "string";
return (
<ThemeProvider {...getThemeProviderProps({ props, isEmbedMode, embedNamespace, router })}>
<ThemeProvider {...getThemeProviderProps({ props, isEmbedMode, embedNamespace })}>
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
{typeof window !== "undefined" && !isEmbedMode && (
@@ -143,16 +155,14 @@ function getThemeProviderProps({
props,
isEmbedMode,
embedNamespace,
router,
}: {
props: Omit<CalcomThemeProps, "children">;
isEmbedMode: boolean;
embedNamespace: string | null;
router: NextRouter;
}) {
const isBookingPage = (() => {
if (typeof props.isBookingPage === "function") {
return props.isBookingPage({ router: router });
return props.isBookingPage({ router: props.router });
}
return props.isBookingPage;
})();
@@ -213,27 +223,52 @@ function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
}
function useOrgBrandingValues() {
const session = useSession();
const res = trpc.viewer.organizations.getBrand.useQuery(undefined, {
// Only fetch if we have a session to avoid flooding logs with errors
enabled: session.status === "authenticated",
});
if (res.status === "loading") {
return undefined;
}
if (res.status === "error") return null;
return res.data;
}
function OrgBrandProvider({ children }: { children: React.ReactNode }) {
const orgBrand = useOrgBrandingValues();
return <OrgBrandingProvider value={orgBrand}>{children}</OrgBrandingProvider>;
return <OrgBrandingProvider value={{ orgBrand }}>{children}</OrgBrandingProvider>;
}
const AppProviders = (props: AppPropsWithChildren) => {
const session = trpc.viewer.public.session.useQuery().data;
// No need to have intercom on public pages - Good for Page Performance
const isPublicPage = usePublicPage();
const isBookingPage = useIsBookingPage();
const { pageProps, ...rest } = props;
const { _nonce, ...restPageProps } = pageProps;
const propsWithoutNonce = {
pageProps: {
...restPageProps,
},
...rest,
};
const RemainingProviders = (
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<SessionProvider session={session || undefined}>
<CustomI18nextProvider {...props}>
<SessionProvider session={pageProps.session ?? undefined}>
<CustomI18nextProvider {...propsWithoutNonce}>
<TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
<CalcomThemeProvider
themeBasis={props.pageProps.themeBasis}
nonce={props.pageProps.nonce}
isThemeSupported={props.Component.isThemeSupported}
isBookingPage={props.Component.isBookingPage}>
isBookingPage={props.Component.isBookingPage || isBookingPage}
router={props.router}>
<FeatureFlagsProvider>
<OrgBrandProvider>
<MetaProvider>{props.children}</MetaProvider>
@@ -246,7 +281,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
</EventCollectionProvider>
);
if (isPublicPage) {
if (isBookingPage) {
return RemainingProviders;
}
+12
View File
@@ -0,0 +1,12 @@
import { usePathname, useSearchParams } from "next/navigation";
export default function useIsBookingPage() {
const pathname = usePathname();
const isBookingPage = ["/booking/", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route));
const searchParams = useSearchParams();
const userParam = searchParams.get("user");
const teamParam = searchParams.get("team");
return !!(isBookingPage || userParam || teamParam);
}
-9
View File
@@ -1,9 +0,0 @@
import { useRouter } from "next/router";
export default function usePublicPage() {
const router = useRouter();
const isPublicPage = ["/[user]", "/booking", "/cancel", "/reschedule"].find((route) =>
router.pathname.startsWith(route)
);
return isPublicPage;
}
+7 -26
View File
@@ -1,36 +1,17 @@
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
export default function useRouterQuery<T extends string>(name: T) {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const existingQueryParams = router.asPath.split("?")[1];
const urlParams = new URLSearchParams(existingQueryParams);
const query: Record<string, string | string[]> = {};
// Following error is thrown by Typescript:
// 'Type 'URLSearchParams' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher'
// We should change the target to higher ES2019 atleast maybe
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
for (const [key, value] of urlParams) {
if (!query[key]) {
query[key] = value;
} else {
let queryValue = query[key];
if (queryValue instanceof Array) {
queryValue.push(value);
} else {
queryValue = query[key] = [queryValue];
queryValue.push(value);
}
}
}
const setQuery = (newValue: string | number | null | undefined) => {
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
const _searchParams = new URLSearchParams(searchParams);
_searchParams.set(name, newValue as string);
router.replace(`${pathname}?${_searchParams.toString()}`);
};
return { [name]: query[name], setQuery } as {
return { [name]: searchParams.get(name), setQuery } as {
[K in T]: string | undefined;
} & { setQuery: typeof setQuery };
}
+3 -25
View File
@@ -1,31 +1,9 @@
import { useRouter } from "next/router";
import { useMemo } from "react";
import { useSearchParams } from "next/navigation";
export function useToggleQuery(name: string) {
const router = useRouter();
const hrefOff = useMemo(() => {
const query = {
...router.query,
};
delete query[name];
return {
query,
};
}, [router.query, name]);
const hrefOn = useMemo(() => {
const query = {
...router.query,
[name]: "1",
};
return {
query,
};
}, [router.query, name]);
const searchParams = useSearchParams();
return {
hrefOn,
hrefOff,
isOn: router.query[name] === "1",
isOn: searchParams?.get(name) === "1",
};
}
+20 -32
View File
@@ -1,22 +1,14 @@
import { get } from "@vercel/edge-config";
import { collectEvents } from "next-collect/server";
import type { NextMiddleware } from "next/server";
import { NextResponse, userAgent } from "next/server";
import { NextResponse } from "next/server";
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 middleware: NextMiddleware = async (req) => {
const url = req.nextUrl;
const requestHeaders = new Headers(req.headers);
if (isIpInBanlist(req) && url.pathname !== "/api/nope") {
// DDOS Prevention: Immediately end request with no response - Avoids a redirect as well initiated by NextAuth on invalid callback
req.nextUrl.pathname = "/api/nope";
return NextResponse.redirect(req.nextUrl);
}
if (!url.pathname.startsWith("/api")) {
//
// NOTE: When tRPC hits an error a 500 is returned, when this is received
@@ -39,25 +31,9 @@ const middleware: NextMiddleware = async (req) => {
}
}
if (["/api/collect-events", "/api/auth"].some((p) => url.pathname.startsWith(p))) {
const callbackUrl = url.searchParams.get("callbackUrl");
const { isBot } = userAgent(req);
if (
isBot ||
(callbackUrl && ![CONSOLE_URL, WEBAPP_URL, WEBSITE_URL].some((u) => callbackUrl.startsWith(u))) ||
isIpInBanlist(req)
) {
// DDOS Prevention: Immediately end request with no response - Avoids a redirect as well initiated by NextAuth on invalid callback
req.nextUrl.pathname = "/api/nope";
return NextResponse.redirect(req.nextUrl);
}
}
// 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);
const res = routingForms.handle(url);
if (res) {
return res;
}
if (url.pathname.startsWith("/api/trpc/")) {
@@ -76,15 +52,27 @@ const middleware: NextMiddleware = async (req) => {
});
};
const routingForms = {
handle: (url: 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);
}
},
};
export const config = {
// Next.js Doesn't support spread operator in config matcher, so, we must list all paths explicitly here.
// https://github.com/vercel/next.js/discussions/42458
matcher: [
"/:path*",
"/api/collect-events/:path*",
"/api/auth/:path*",
"/apps/routing_forms/:path*",
"/:path*/embed",
"/api/trpc/:path*",
"/auth/login",
/**
* Paths required by routingForms.handle
*/
"/apps/routing_forms/:path*",
],
};
+34 -3
View File
@@ -25,8 +25,10 @@ if (!process.env.NEXTAUTH_URL && process.env.NEXT_PUBLIC_WEBAPP_URL) {
if (!process.env.NEXT_PUBLIC_WEBSITE_URL) {
process.env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL;
}
if (process.env.CSP_POLICY === "strict" && process.env.NODE_ENV === "production") {
if (
process.env.CSP_POLICY === "strict" &&
(process.env.CALCOM_ENV === "production" || process.env.NODE_ENV === "production")
) {
throw new Error(
"Strict CSP policy(for style-src) is not yet supported in production. You can experiment with it in Dev Mode"
);
@@ -127,7 +129,10 @@ const matcherConfigUserTypeEmbedRoute = {
/** @type {import("next").NextConfig} */
const nextConfig = {
i18n,
i18n: {
...i18n,
localeDetection: false,
},
productionBrowserSourceMaps: true,
/* We already do type check on GH actions */
typescript: {
@@ -376,6 +381,11 @@ const nextConfig = {
destination: "/signup",
permanent: true,
},
{
source: "/auth",
destination: "/auth/login",
permanent: false,
},
{
source: "/settings",
destination: "/settings/my-account/profile",
@@ -446,6 +456,27 @@ const nextConfig = {
destination: "/apps/installed/conferencing",
permanent: true,
},
// OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL
...(process.env.NODE_ENV === "development" &&
// Safer to enable the redirect only when the user is opting to test out organizations
process.env.ORGANIZATIONS_ENABLED &&
// Prevent infinite redirect by checking that we aren't already on localhost
process.env.NEXT_PUBLIC_WEBAPP_URL !== "http://localhost:3000"
? [
{
has: [
{
type: "header",
key: "host",
value: "localhost:3000",
},
],
source: "/api/integrations/:args*",
destination: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/integrations/:args*`,
permanent: false,
},
]
: []),
];
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.1.2",
"version": "3.1.9",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -58,7 +58,6 @@
"@radix-ui/react-tooltip": "^1.0.0",
"@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0",
"@tanem/react-nprogress": "^5.0.44",
"@tanstack/react-query": "^4.3.9",
"@tremor/react": "^2.0.0",
"@types/turndown": "^5.0.1",
+11 -25
View File
@@ -1,13 +1,14 @@
import type { GetStaticPropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { orgDomainConfig, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
import { DOCS_URL, JOIN_DISCORD, WEBSITE_URL, IS_CALCOM } from "@calcom/lib/constants";
import { DOCS_URL, IS_CALCOM, JOIN_DISCORD, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui";
import { BookOpen, Check, ChevronRight, FileText, Shield } from "@calcom/ui/components/icon";
import { Discord } from "@calcom/ui/components/icon/Discord";
import PageWrapper from "@components/PageWrapper";
@@ -21,9 +22,8 @@ enum pageType {
}
export default function Custom404() {
const pathname = usePathname();
const { t } = useLocale();
const router = useRouter();
const [username, setUsername] = useState<string>("");
const [currentPageType, setCurrentPageType] = useState<pageType>(pageType.USER);
@@ -51,7 +51,7 @@ export default function Custom404() {
const [url, setUrl] = useState(`${WEBSITE_URL}/signup`);
useEffect(() => {
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(window.location.host);
const [routerUsername] = router.asPath.replace("%20", "-").split(/[?#]/);
const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/);
if (!isValidOrgDomain || !currentOrgDomain) {
const splitPath = routerUsername.split("/");
if (splitPath[1] === "team" && splitPath.length === 3) {
@@ -77,14 +77,14 @@ export default function Custom404() {
}
}, []);
const isSuccessPage = router.asPath.startsWith("/booking");
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
const isSignup = router.asPath.startsWith("/signup");
const isSuccessPage = pathname?.startsWith("/booking");
const isSubpage = pathname?.includes("/", 2) || isSuccessPage;
const isSignup = pathname?.startsWith("/signup");
/**
* If we're on 404 and the route is insights it means it is disabled
* TODO: Abstract this for all disabled features
**/
const isInsights = router.asPath.startsWith("/insights");
const isInsights = pathname?.startsWith("/insights");
if (isInsights) {
return (
<>
@@ -205,14 +205,7 @@ export default function Custom404() {
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<svg
className="text-default h-6 w-6"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 127.14 96.36">
<title>Discord Logo</title>
<path d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z" />
</svg>
<Discord className="text-default h-6 w-6" />
</span>
</div>
<div className="min-w-0 flex-1">
@@ -350,14 +343,7 @@ export default function Custom404() {
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<svg
className="text-default h-6 w-6"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 127.14 96.36">
<title>Discord Logo</title>
<path d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z" />
</svg>
<Discord className="text-default h-6 w-6" />
</span>
</div>
<div className="min-w-0 flex-1">
+10 -13
View File
@@ -1,7 +1,7 @@
import Head from "next/head";
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast } from "@calcom/ui";
import { Copy } from "@calcom/ui/components/icon";
@@ -9,8 +9,8 @@ import { Copy } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
export default function Error500() {
const searchParams = useSearchParams();
const { t } = useLocale();
const router = useRouter();
return (
<div className="bg-subtle flex h-screen">
@@ -21,24 +21,21 @@ export default function Error500() {
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
<h1 className="font-cal text-emphasis text-6xl">500</h1>
<h2 className="text-emphasis mt-6 text-2xl font-medium">It&apos;s not you, it&apos;s us.</h2>
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
away for you.
</p>
{router.query.error && (
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">{t("something_went_wrong_on_our_end")}</p>
{searchParams?.get("error") && (
<div className="mb-8 flex flex-col">
<p className="text-default mb-4 max-w-2xl text-sm">
Please provide the following text when contacting support to better help you:
{t("please_provide_following_text_to_suppport")}:
</p>
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
{router.query.error}
{searchParams?.get("error")}
<br />
<Button
color="secondary"
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
StartIcon={Copy}
onClick={() => {
navigator.clipboard.writeText(router.query.error as string);
navigator.clipboard.writeText(searchParams?.get("error") as string);
showToast("Link copied!", "success");
}}>
{t("copy")}
@@ -46,9 +43,9 @@ export default function Error500() {
</pre>
</div>
)}
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
<Button href="mailto:support@cal.com">{t("contact_support")}</Button>
<Button color="secondary" href="javascript:history.back()" className="ml-2">
Go back
{t("go_back")}
</Button>
</div>
</div>
+133 -161
View File
@@ -1,8 +1,9 @@
import type { DehydratedState } from "@tanstack/react-query";
import classNames from "classnames";
import type { GetServerSidePropsContext } from "next";
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { Toaster } from "react-hot-toast";
import type { z } from "zod";
import {
sdkActionManager,
@@ -10,99 +11,47 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import defaultEvents, {
getDynamicEventDescription,
getGroupName,
getUsernameList,
getUsernameSlugLink,
} from "@calcom/lib/defaultEvents";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import prisma from "@calcom/prisma";
import type { EventType, User } from "@calcom/prisma/client";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
import { Avatar, HeadSeo, UnpublishedEntity } from "@calcom/ui";
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import { ssrInit } from "@server/lib/ssr";
export type UserPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export function UserPage(props: UserPageProps) {
const {
users,
profile,
eventTypes,
isDynamicGroup,
dynamicNames,
dynamicUsernames,
isSingleUser,
markdownStrippedBio,
} = props;
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
useTheme(user.theme);
useTheme(profile.theme);
const { t } = useLocale();
const router = useRouter();
const isBioEmpty = !user.bio || !user.bio.replace("<p><br></p>", "").length;
const groupEventTypes = props.users.some((user) => !user.allowDynamicBooking) ? (
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border ">
<div className="text-muted p-8 text-center">
<h2 className="font-cal text-default mb-2 text-3xl">{" " + t("unavailable")}</h2>
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled") as string}</p>
</div>
</div>
</div>
) : (
<ul>
{eventTypes.map((type, index) => (
<li
key={index}
className=" border-subtle bg-default dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
<ArrowRight className="text-emphasis absolute right-3 top-3 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
<Link
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
className="flex justify-between px-6 py-4"
data-testid="event-type-link">
<div className="flex-shrink">
<p className=" text-emphasis text-sm font-semibold">{type.title}</p>
<EventTypeDescription className="text-sm" eventType={type} />
</div>
<div className="mt-1 self-center">
<AvatarGroup
truncateAfter={4}
className="flex flex-shrink-0"
size="sm"
items={props.users.map((user) => ({
alt: user.name || "",
image: user.avatar,
}))}
/>
</div>
</Link>
</li>
))}
</ul>
);
const isEmbed = useIsEmbed(props.isEmbed);
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const query = { ...router.query };
delete query.user; // So it doesn't display in the Link (and make tests fail)
delete query.orgSlug;
const {
// So it doesn't display in the Link (and make tests fail)
user: _user,
orgSlug: _orgSlug,
...query
} = useRouterQuery();
const nameOrUsername = user.name || user.username || "";
/*
@@ -113,18 +62,25 @@ export function UserPage(props: UserPageProps) {
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
}
}, [telemetry, router.asPath]); */
if (entity?.isUnpublished) {
return (
<div className="flex h-full min-h-[100dvh] items-center justify-center">
<UnpublishedEntity {...entity} />
</div>
);
}
const isEventListEmpty = eventTypes.length === 0;
return (
<>
<HeadSeo
title={isDynamicGroup ? dynamicNames.join(", ") : nameOrUsername}
description={isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : markdownStrippedBio}
title={profile.name}
description={markdownStrippedBio}
meeting={{
title: isDynamicGroup ? "" : markdownStrippedBio,
title: markdownStrippedBio,
profile: { name: `${profile.name}`, image: null },
users: isDynamicGroup
? dynamicUsernames.map((username, index) => ({ username, name: dynamicNames[index] }))
: [{ username: `${user.username}`, name: `${user.name}` }],
users: [{ username: `${user.username}`, name: `${user.name}` }],
}}
/>
@@ -135,25 +91,23 @@ export function UserPage(props: UserPageProps) {
isEmbed ? "border-booker border-booker-width bg-default rounded-md border" : "",
"max-w-3xl px-4 py-24"
)}>
{isSingleUser && ( // When we deal with a single user, not dynamic group
<div className="mb-8 text-center">
<Avatar imageSrc={user.avatar} size="xl" alt={nameOrUsername} />
<h1 className="font-cal text-emphasis mb-1 text-3xl">
{nameOrUsername}
{user.verified && (
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black" />
)}
</h1>
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: props.safeBio }}
/>
</>
<div className="mb-8 text-center">
<Avatar imageSrc={profile.image} size="xl" alt={profile.name} />
<h1 className="font-cal text-emphasis mb-1 text-3xl">
{profile.name}
{user.verified && (
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black" />
)}
</div>
)}
</h1>
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: props.safeBio }}
/>
</>
)}
</div>
<div
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
@@ -165,8 +119,6 @@ export function UserPage(props: UserPageProps) {
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
</div>
</div>
) : isDynamicGroup ? ( //When we deal with dynamic group (users > 1)
groupEventTypes
) : (
eventTypes.map((type) => (
<div
@@ -200,7 +152,7 @@ export function UserPage(props: UserPageProps) {
)}
</div>
{isEventListEmpty && <EmptyPage name={user.name ?? "User"} />}
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
</main>
<Toaster position="bottom-right" />
</div>
@@ -254,11 +206,48 @@ const getEventTypesWithHiddenFromDB = async (userId: number) => {
}));
};
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const crypto = await import("crypto");
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
export type UserPageProps = {
trpcState: DehydratedState;
profile: {
name: string;
image: string;
theme: string | null;
brandColor: string;
darkBrandColor: string;
};
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[];
themeBasis: string | null;
markdownStrippedBio: string;
safeBio: string;
entity: {
isUnpublished?: boolean;
orgSlug?: string | null;
name?: string | null;
};
eventTypes: ({
descriptionAsSafeHTML: string;
metadata: z.infer<typeof EventTypeMetaDataSchema>;
} & Pick<
EventType,
| "id"
| "title"
| "slug"
| "length"
| "hidden"
| "requiresConfirmation"
| "requiresBookerEmailVerification"
| "price"
| "currency"
| "recurringEvent"
>)[];
} & EmbedProps;
export const getServerSideProps: GetServerSideProps<UserPageProps> = async (context) => {
const ssr = await ssrInit(context);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
context.req.headers.host ?? "",
context.params?.orgSlug
);
const usernameList = getUsernameList(context.query.user as string);
const dataFetchStart = Date.now();
const usersWithoutAvatar = await prisma.user.findMany({
@@ -266,11 +255,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
username: {
in: usernameList,
},
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
organization: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
},
select: {
id: true,
@@ -281,6 +266,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
brandColor: true,
darkBrandColor: true,
organizationId: true,
organization: {
select: {
slug: true,
name: true,
},
},
theme: true,
away: true,
verified: true,
@@ -288,6 +279,21 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
});
const isDynamicGroup = usersWithoutAvatar.length > 1;
if (isDynamicGroup) {
return {
redirect: {
permanent: false,
destination: `/${usernameList.join("+")}/dynamic`,
},
} as {
redirect: {
permanent: false;
destination: string;
};
};
}
const users = usersWithoutAvatar.map((user) => ({
...user,
avatar: `/${user.username}/avatar.png`,
@@ -300,45 +306,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
notFound: true;
};
}
const isDynamicGroup = users.length > 1;
if (isDynamicGroup) {
// sort and be in the same order as usernameList so first user is the first user in the list
users.sort((a, b) => {
const aIndex = (a.username && usernameList.indexOf(a.username)) || 0;
const bIndex = (b.username && usernameList.indexOf(b.username)) || 0;
return aIndex - bIndex;
});
}
const dynamicNames = isDynamicGroup
? users.map((user) => {
return user.name || "";
})
: [];
const [user] = users; //to be used when dealing with single user, not dynamic group
const profile = isDynamicGroup
? {
name: getGroupName(dynamicNames),
image: null,
theme: null,
weekStart: "Sunday",
brandColor: "",
darkBrandColor: "",
allowDynamicBooking: !users.some((user) => {
return !user.allowDynamicBooking;
}),
}
: {
name: user.name || user.username,
image: user.avatar,
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
};
const profile = {
name: user.name || user.username || "",
image: user.avatar,
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
};
const eventTypesWithHidden = isDynamicGroup ? [] : await getEventTypesWithHiddenFromDB(user.id);
const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id);
const dataFetchEnd = Date.now();
if (context.query.log === "1") {
context.res.setHeader("X-Data-Fetch-Time", `${dataFetchEnd - dataFetchStart}ms`);
@@ -351,38 +330,31 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}));
const isSingleUser = users.length === 1;
const dynamicUsernames = isDynamicGroup
? users.map((user) => {
return user.username || "";
})
: [];
const safeBio = markdownToSafeHTML(user.bio) || "";
const markdownStrippedBio = stripMarkdown(user?.bio || "");
const org = usersWithoutAvatar[0].organization;
return {
props: {
users,
users: users.map((user) => ({
name: user.name,
username: user.username,
bio: user.bio,
away: user.away,
verified: user.verified,
})),
entity: {
isUnpublished: org?.slug === null,
orgSlug: currentOrgDomain,
name: org?.name ?? null,
},
eventTypes,
safeBio,
profile,
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: isDynamicGroup ? null : user.username,
user: {
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
},
eventTypes: isDynamicGroup
? defaultEvents.map((event) => {
event.description = getDynamicEventDescription(dynamicUsernames, event.slug);
return event;
})
: eventTypes,
themeBasis: user.username,
trpcState: ssr.dehydrate(),
isDynamicGroup,
dynamicNames,
dynamicUsernames,
isSingleUser,
markdownStrippedBio,
},
};
+48 -20
View File
@@ -2,31 +2,46 @@ import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { Booker } from "@calcom/atoms";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
import {
getBookingForReschedule,
getBookingForSeatedEvent,
getMultipleDurationValue,
} from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { classNames } from "@calcom/lib";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export default function Type({ slug, user, booking, away, isBrandingHidden, rescheduleUid, org }: PageProps) {
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
export default function Type({
slug,
user,
isEmbed,
booking,
away,
isBrandingHidden,
rescheduleUid,
entity,
duration,
}: PageProps) {
return (
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={rescheduleUid ?? undefined}
hideBranding={isBrandingHidden}
org={org}
entity={entity}
/>
<Booker
username={user}
@@ -34,7 +49,8 @@ export default function Type({ slug, user, booking, away, isBrandingHidden, resc
bookingData={booking}
isAway={away}
hideBranding={isBrandingHidden}
org={org}
entity={entity}
duration={duration}
/>
</main>
);
@@ -45,11 +61,14 @@ Type.PageWrapper = PageWrapper;
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const { rescheduleUid, bookingUid } = context.query;
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
context.req.headers.host ?? "",
context.params?.orgSlug
);
const users = await prisma.user.findMany({
where: {
@@ -97,7 +116,12 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
return {
props: {
org,
entity: eventData.entity,
duration: getMultipleDurationValue(
eventData.metadata?.multipleDuration,
queryDuration,
eventData.length
),
booking,
user: usernames.join("+"),
slug,
@@ -114,19 +138,18 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
async function getUserPageProps(context: GetServerSidePropsContext) {
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const username = usernames[0];
const { rescheduleUid, bookingUid } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
context.req.headers.host ?? "",
context.params?.orgSlug
);
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
organization: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
},
select: {
away: true,
@@ -149,7 +172,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
const org = isValidOrgDomain ? currentOrgDomain : null;
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
// as well as to check if the event exist, so we can show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({
username,
eventSlug: slug,
@@ -165,10 +188,15 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
return {
props: {
booking,
duration: getMultipleDurationValue(
eventData.metadata?.multipleDuration,
queryDuration,
eventData.length
),
away: user?.away,
user: username,
slug,
org,
entity: eventData.entity,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
themeBasis: username,
+2
View File
@@ -1,3 +1,5 @@
import React from "react";
import { trpc } from "@calcom/trpc/react";
import type { AppProps } from "@lib/app-providers";
+7 -4
View File
@@ -3,6 +3,8 @@ import type { DocumentContext, DocumentProps } from "next/document";
import Document, { Head, Html, Main, NextScript } from "next/document";
import { z } from "zod";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { csp } from "@lib/csp";
type Props = Record<string, unknown> & DocumentProps;
@@ -28,17 +30,18 @@ class MyDocument extends Document<Props> {
// Use a dummy URL as default so that URL parsing works for relative URLs as well. We care about searchParams and pathname only
const parsedUrl = new URL(asPath, "https://dummyurl");
const isEmbed = parsedUrl.pathname.endsWith("/embed") || parsedUrl.searchParams.get("embedType") !== null;
const embedColorScheme = parsedUrl.searchParams.get("ui.color-scheme");
const initialProps = await Document.getInitialProps(ctx);
return { isEmbed, nonce, ...initialProps };
return { isEmbed, embedColorScheme, nonce, ...initialProps };
}
render() {
const { locale } = this.props.__NEXT_DATA__;
const { isEmbed } = this.props;
const { isEmbed, embedColorScheme } = this.props;
const nonceParsed = z.string().safeParse(this.props.nonce);
const nonce = nonceParsed.success ? nonceParsed.data : "";
return (
<Html lang={locale}>
<Html lang={locale} style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}>
<Head nonce={nonce}>
<link rel="apple-touch-icon" sizes="180x180" href="/api/logo?type=apple-touch-icon" />
<link rel="icon" type="image/png" sizes="32x32" href="/api/logo?type=favicon-32" />
@@ -48,7 +51,7 @@ class MyDocument extends Document<Props> {
<meta name="msapplication-TileColor" content="#ff0000" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
{(process.env.NODE_ENV === "development" || process.env.VERCEL_ENV === "preview") && (
{(!IS_PRODUCTION || process.env.VERCEL_ENV === "preview") && (
// eslint-disable-next-line @next/next/no-sync-scripts
<script
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4"
+11 -3
View File
@@ -10,6 +10,7 @@ import React from "react";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { redactError } from "@calcom/lib/redactError";
import { ErrorPage } from "@components/error/error-page";
@@ -59,10 +60,17 @@ CustomError.getInitialProps = async (ctx: AugmentedNextPageContext) => {
// If a HttpError message, let's override defaults
if (err instanceof HttpError) {
const redactedError = redactError(err);
errorInitialProps.statusCode = err.statusCode;
errorInitialProps.title = err.name;
errorInitialProps.message = err.message;
errorInitialProps.err = err;
errorInitialProps.title = redactedError.name;
errorInitialProps.message = redactedError.message;
errorInitialProps.err = {
...redactedError,
url: err.url,
statusCode: err.statusCode,
cause: err.cause,
method: err.method,
};
}
if (res) {
+9 -60
View File
@@ -1,13 +1,9 @@
import type { ResetPasswordRequest } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { sendPasswordResetEmail } from "@calcom/emails";
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
import { passwordResetRequest } from "@calcom/features/auth/lib/passwordResetRequest";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { defaultHandler } from "@calcom/lib/server";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -37,63 +33,16 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
});
try {
const maybeUser = await prisma.user.findUnique({
where: {
email: email.data,
},
select: {
name: true,
identityProvider: true,
email: true,
locale: true,
},
const user = await prisma.user.findUnique({
where: { email: email.data },
select: { name: true, email: true, locale: true },
});
if (!maybeUser) {
// Don't leak information about whether an email is registered or not
return res
.status(200)
.json({ message: "If this email exists in our system, you should receive a Reset email." });
}
const t = await getTranslation(maybeUser.locale ?? "en", "common");
const maybePreviousRequest = await prisma.resetPasswordRequest.findMany({
where: {
email: maybeUser.email,
expires: {
gt: new Date(),
},
},
});
let passwordRequest: ResetPasswordRequest;
if (maybePreviousRequest && maybePreviousRequest?.length >= 1) {
passwordRequest = maybePreviousRequest[0];
} else {
const expiry = dayjs().add(PASSWORD_RESET_EXPIRY_HOURS, "hours").toDate();
const createdResetPasswordRequest = await prisma.resetPasswordRequest.create({
data: {
email: maybeUser.email,
expires: expiry,
},
});
passwordRequest = createdResetPasswordRequest;
}
const resetLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/forgot-password/${passwordRequest.id}`;
await sendPasswordResetEmail({
language: t,
user: maybeUser,
resetLink,
});
return res
.status(201)
.json({ message: "If this email exists in our system, you should receive a Reset email." });
// Don't leak info about whether the user exists
if (!user) return res.status(201).json({ message: "password_reset_email_sent" });
await passwordResetRequest(user);
return res.status(201).json({ message: "password_reset_email_sent" });
} catch (reason) {
// console.error(reason);
console.error(reason);
return res.status(500).json({ message: "Unable to create password reset request" });
}
}
+94 -55
View File
@@ -2,8 +2,10 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
import { IS_CALCOM } from "@calcom/lib/constants";
import slugify from "@calcom/lib/slugify";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import { validateUsernameInOrg } from "@calcom/lib/validateUsernameInOrg";
@@ -12,7 +14,9 @@ import { IdentityProvider } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
const signupSchema = z.object({
username: z.string(),
username: z.string().refine((value) => !value.includes("+"), {
message: "String should not contain a plus symbol (+).",
}),
email: z.string().email(),
password: z.string().min(7),
language: z.string().optional(),
@@ -21,7 +25,7 @@ const signupSchema = z.object({
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return;
return res.status(405).end();
}
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
@@ -40,37 +44,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return;
}
// There is an existingUser if the username matches
// OR if the email matches AND either the email is verified
// or both username and password are set
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ username },
{
AND: [
{ email: userEmail },
{
OR: [
{ emailVerified: { not: null } },
{
AND: [{ password: { not: null } }, { username: { not: null } }],
},
],
},
],
},
],
},
});
if (existingUser) {
const message: string =
existingUser.email !== userEmail ? "Username already taken" : "Email address is already registered";
return res.status(409).json({ message });
}
let foundToken: { id: number; teamId: number | null; expires: Date } | null = null;
if (token) {
foundToken = await prisma.verificationToken.findFirst({
@@ -98,26 +71,40 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(409).json({ message: "Username already taken" });
}
}
} else {
// There is an existingUser if the username matches
// OR if the email matches AND either the email is verified
// or both username and password are set
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ username },
{
AND: [
{ email: userEmail },
{
OR: [
{ emailVerified: { not: null } },
{
AND: [{ password: { not: null } }, { username: { not: null } }],
},
],
},
],
},
],
},
});
if (existingUser) {
const message: string =
existingUser.email !== userEmail ? "Username already taken" : "Email address is already registered";
return res.status(409).json({ message });
}
}
const hashedPassword = await hashPassword(password);
const user = await prisma.user.upsert({
where: { email: userEmail },
update: {
username,
password: hashedPassword,
emailVerified: new Date(Date.now()),
identityProvider: IdentityProvider.CAL,
},
create: {
username,
email: userEmail,
password: hashedPassword,
identityProvider: IdentityProvider.CAL,
},
});
if (foundToken && foundToken?.teamId) {
const team = await prisma.team.findUnique({
where: {
@@ -127,6 +114,34 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (team) {
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
if (IS_CALCOM && (!teamMetadata?.isOrganization || !!team.parentId)) {
const checkUsername = await checkPremiumUsername(username);
if (checkUsername.premium) {
// This signup page is ONLY meant for team invites and local setup. Not for every day users.
// In singup redesign/refactor coming up @sean will tackle this to make them the same API/page instead of two.
return res.status(422).json({
message: "Sign up from https://cal.com/signup to claim your premium username",
});
}
}
const user = await prisma.user.upsert({
where: { email: userEmail },
update: {
username,
password: hashedPassword,
emailVerified: new Date(Date.now()),
identityProvider: IdentityProvider.CAL,
},
create: {
username,
email: userEmail,
password: hashedPassword,
identityProvider: IdentityProvider.CAL,
},
});
if (teamMetadata?.isOrganization) {
await prisma.user.update({
where: {
@@ -196,13 +211,37 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: foundToken.id,
},
});
} else {
if (IS_CALCOM) {
const checkUsername = await checkPremiumUsername(username);
if (checkUsername.premium) {
res.status(422).json({
message: "Sign up from https://cal.com/signup to claim your premium username",
});
return;
}
}
await prisma.user.upsert({
where: { email: userEmail },
update: {
username,
password: hashedPassword,
emailVerified: new Date(Date.now()),
identityProvider: IdentityProvider.CAL,
},
create: {
username,
email: userEmail,
password: hashedPassword,
identityProvider: IdentityProvider.CAL,
},
});
await sendEmailVerification({
email: userEmail,
username,
language,
});
}
await sendEmailVerification({
email: userEmail,
username,
language,
});
res.status(201).json({ message: "Created user" });
}
@@ -6,6 +6,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/client";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
@@ -28,7 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(401).json({ message: "Not authenticated" });
}
if (!user.password) {
if (!user.password && user.identityProvider === IdentityProvider.CAL) {
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
}
@@ -36,9 +37,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.json({ message: "Two factor disabled" });
}
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
if (!isCorrectPassword) {
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
if (user.password && user.identityProvider === IdentityProvider.CAL) {
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
if (!isCorrectPassword) {
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
}
}
// if user has 2fa
if (user.twoFactorEnabled) {
@@ -31,6 +31,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
// Only send reminders if the event hasn't finished
endTime: { gte: new Date() },
OR: [
// no payment required
{
payment: { none: {} },
},
// paid but awaiting approval
{
payment: { some: {} },
paid: true,
},
],
},
select: {
...bookingMinimalSelect,
+3 -2
View File
@@ -1,10 +1,11 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { renderEmail } from "@calcom/emails";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getTranslation } from "@calcom/lib/server/i18n";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (process.env.NODE_ENV !== "development") return res.write("Only for development purposes"), res.end();
if (IS_PRODUCTION) return res.write("Only for development purposes"), res.end();
const t = await getTranslation("en", "common");
const language = { translate: t, locale: "en" };
@@ -55,7 +56,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
],
};
req.statusCode = 200;
res.statusCode = 200;
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
+4 -1
View File
@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import type { Session } from "next-auth";
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
import { HttpError } from "@calcom/lib/http-error";
@@ -37,6 +38,9 @@ const defaultIntegrationAddHandler = async ({
throw new Error("App is already installed");
}
}
await throwIfNotHaveAdminAccessToTeam({ teamId: teamId ?? null, userId: user.id });
await createCredential({ user: user, appType, slug, teamId });
};
@@ -72,7 +76,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return res.status(200);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
return res.status(error.statusCode).json({ message: error.message });
}
+10 -1
View File
@@ -9,6 +9,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { defaultHandler } from "@calcom/lib/server";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
@@ -92,6 +93,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
eventType: {
select: {
teamId: true,
parentId: true,
},
},
user: {
@@ -168,13 +170,20 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
uid: booking.uid,
};
const teamId = await getTeamIdFromEventType({
eventType: {
team: { id: booking?.eventType?.teamId ?? null },
parentId: booking?.eventType?.parentId ?? null,
},
});
await triggerWebhook({
evt,
downloadLink,
booking: {
userId: booking?.user?.id,
eventTypeId: booking.eventTypeId,
teamId: booking.eventType?.teamId,
teamId,
},
});
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
@@ -22,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { code, state } = req.query;
const parsedState = stateSchema.parse(JSON.parse(state as string));
const { teamId } = parsedState;
await throwIfNotHaveAdminAccessToTeam({ teamId: Number(teamId) ?? null, userId: session.user.id });
if (code && typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
+1 -1
View File
@@ -1,4 +1,4 @@
import { featureFlagRouter } from "@calcom/features/flags/server/router";
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
export default createNextApiHandler(featureFlagRouter);
export default createNextApiHandler(featureFlagRouter, true, "features");
@@ -0,0 +1,4 @@
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
import { kycVerificationRouter } from "@calcom/trpc/server/routers/viewer/kycVerification/_router";
export default createNextApiHandler(kycVerificationRouter);
+19 -12
View File
@@ -2,7 +2,7 @@ import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import prisma from "@calcom/prisma";
@@ -12,22 +12,33 @@ const querySchema = z
.object({
username: z.string(),
teamname: z.string(),
/**
* Allow fetching avatar of a particular organization
* Avatars being public, we need not worry about others accessing it.
*/
orgId: z.string().transform((s) => Number(s)),
})
.partial();
async function getIdentityData(req: NextApiRequest) {
const { username, teamname } = querySchema.parse(req.query);
const { username, teamname, orgId } = querySchema.parse(req.query);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
const org = isValidOrgDomain ? currentOrgDomain : null;
const orgQuery = orgId
? {
id: orgId,
}
: org
? getSlugOrRequestedSlug(org)
: null;
if (username) {
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
organization: orgQuery,
},
select: { avatar: true, email: true },
});
@@ -42,11 +53,7 @@ async function getIdentityData(req: NextApiRequest) {
const team = await prisma.team.findFirst({
where: {
slug: teamname,
parent: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
parent: orgQuery,
},
select: { logo: true },
});
+11 -4
View File
@@ -1,9 +1,10 @@
import type { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import prisma from "@calcom/prisma";
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
@@ -58,8 +59,8 @@ function getRoute(appName: string, pages: string[]) {
const AppPage: AppPageType["default"] = function AppPage(props) {
const appName = props.appName;
const router = useRouter();
const pages = router.query.pages as string[];
const params = useParamsWithFallback();
const pages = (params.pages || []) as string[];
const route = getRoute(appName, pages);
const componentProps = {
@@ -127,6 +128,12 @@ export async function getServerSideProps(
params.appPages = pages.slice(1);
const session = await getServerSession({ req, res });
const user = session?.user;
const app = await getAppWithMetadata({ slug: appName });
if (!app) {
return {
notFound: true,
};
}
const result = await route.getServerSideProps(
context as GetServerSidePropsContext<{
@@ -153,7 +160,7 @@ export async function getServerSideProps(
return {
props: {
appName,
appUrl: `/apps/${appName}`,
appUrl: app.simplePath || `/apps/${appName}`,
...result.props,
},
};
+4 -2
View File
@@ -9,6 +9,7 @@ import { z } from "zod";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -38,7 +39,7 @@ const sourceSchema = z.object({
function SingleAppPage(props: inferSSRProps<typeof getStaticProps>) {
// If it's not production environment, it would be a better idea to inform that the App is disabled.
if (props.isAppDisabled) {
if (process.env.NODE_ENV !== "production") {
if (!IS_PRODUCTION) {
// TODO: Improve disabled App UI. This is just a placeholder.
return (
<div className="p-2">
@@ -77,6 +78,7 @@ function SingleAppPage(props: inferSSRProps<typeof getStaticProps>) {
descriptionItems={source.data?.items as string[] | undefined}
isTemplate={data.isTemplate}
dependencies={data.dependencies}
concurrentMeetings={data.concurrentMeetings}
// tos="https://zoom.us/terms"
// privacy="https://zoom.us/privacy"
body={
@@ -122,7 +124,7 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
const isAppAvailableInFileSystem = appMeta;
const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled);
if (process.env.NODE_ENV !== "production" && isAppDisabled) {
if (!IS_PRODUCTION && isAppDisabled) {
return {
props: {
isAppDisabled: true as const,
+6 -7
View File
@@ -1,6 +1,6 @@
import type { GetStaticPaths, InferGetStaticPropsType } from "next";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import { AppSetupPage } from "@calcom/app-store/_pages/setup";
import { getStaticProps } from "@calcom/app-store/_pages/setup/_getStaticProps";
@@ -9,8 +9,9 @@ import { HeadSeo } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
export default function SetupInformation(props: InferGetStaticPropsType<typeof getStaticProps>) {
const searchParams = useSearchParams();
const router = useRouter();
const slug = router.query.slug as string;
const slug = searchParams?.get("slug") as string;
const { status } = useSession();
if (status === "loading") {
@@ -18,12 +19,10 @@ export default function SetupInformation(props: InferGetStaticPropsType<typeof g
}
if (status === "unauthenticated") {
router.replace({
pathname: "/auth/login",
query: {
callbackUrl: `/apps/${slug}/setup`,
},
const urlSearchParams = new URLSearchParams({
callbackUrl: `/apps/${slug}/setup`,
});
router.replace(`/auth/login?${urlSearchParams.toString()}`);
}
return (
@@ -1,7 +1,7 @@
import { Prisma } from "@prisma/client";
import type { GetStaticPropsContext, InferGetStaticPropsType } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import Shell from "@calcom/features/shell/Shell";
@@ -13,9 +13,9 @@ import { AppCard, SkeletonText } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) {
const searchParams = useSearchParams();
const { t, isLocaleReady } = useLocale();
const router = useRouter();
const { category } = router.query;
const category = searchParams?.get("category");
return (
<>
+2
View File
@@ -3,6 +3,7 @@ import type { ChangeEventHandler } from "react";
import { useState } from "react";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getLayout } from "@calcom/features/MainLayout";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
@@ -94,6 +95,7 @@ export default function Apps({
}
Apps.PageWrapper = PageWrapper;
Apps.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
+4 -5
View File
@@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import { useCallback, useReducer, useState } from "react";
import z from "zod";
@@ -169,7 +169,7 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{!appIsDefault && variant === "conferencing" && (
{!appIsDefault && variant === "conferencing" && !item.credentialOwner?.teamId && (
<DropdownMenuItem>
<DropdownItem
type="button"
@@ -350,10 +350,9 @@ type ModalState = {
};
export default function InstalledApps() {
const searchParams = useSearchParams();
const { t } = useLocale();
const router = useRouter();
const category = router.query.category as querySchemaType["category"];
const category = searchParams?.get("category") as querySchemaType["category"];
const categoryList: AppCategories[] = Object.values(AppCategories).filter((category) => {
// Exclude calendar and other from categoryList, we handle those slightly differently below
return !(category in { other: null, calendar: null });
+5 -9
View File
@@ -1,11 +1,10 @@
import type { GetStaticPropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import type { ReactElement } from "react";
import { useSearchParams } from "next/navigation";
import z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, SkeletonText } from "@calcom/ui";
import { Button } from "@calcom/ui";
import { X } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
@@ -19,13 +18,10 @@ const querySchema = z.object({
export default function Error() {
const { t } = useLocale();
const router = useRouter();
const { error } = querySchema.parse(router.query);
const searchParams = useSearchParams();
const { error } = querySchema.parse(searchParams);
const isTokenVerificationError = error?.toLowerCase() === "verification";
let errorMsg: string | ReactElement = <SkeletonText />;
if (router.isReady) {
errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
}
const errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
return (
<AuthContainer title="" description="">
+3 -2
View File
@@ -3,7 +3,6 @@ import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Link from "next/link";
import type { CSSProperties } from "react";
import React from "react";
import { useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -24,6 +23,8 @@ export default function Page({ requestId, isRequestExpired, csrfToken }: Props)
const formMethods = useForm<{ new_password: string }>();
const success = formMethods.formState.isSubmitSuccessful;
const loading = formMethods.formState.isSubmitting;
const passwordValue = formMethods.watch("new_password");
const isEmpty = passwordValue?.length === 0;
const submitChangePassword = async ({ password, requestId }: { password: string; requestId: string }) => {
const res = await fetch("/api/auth/reset-password", {
@@ -123,7 +124,7 @@ export default function Page({ requestId, isRequestExpired, csrfToken }: Props)
loading={loading}
color="primary"
type="submit"
disabled={loading}
disabled={loading || isEmpty}
className="w-full justify-center">
{t("reset_password")}
</Button>
@@ -3,7 +3,6 @@ import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Link from "next/link";
import { useRouter } from "next/router";
import type { CSSProperties, SyntheticEvent } from "react";
import React from "react";
@@ -20,7 +19,6 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
const [error, setError] = React.useState<{ message: string } | null>(null);
const [success, setSuccess] = React.useState(false);
const [email, setEmail] = React.useState("");
const router = useRouter();
const handleChange = (e: SyntheticEvent) => {
const target = e.target as typeof e.target & { value: string };
@@ -40,8 +38,6 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
const json = await res.json();
if (!res.ok) {
setError(json);
} else if ("resetLink" in json) {
router.push(json.resetLink);
} else {
setSuccess(true);
}
+5 -4
View File
@@ -4,7 +4,7 @@ import { jwtVerify } from "jose";
import type { GetServerSidePropsContext } from "next";
import { getCsrfToken, signIn } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import type { CSSProperties } from "react";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
@@ -49,6 +49,7 @@ export default function Login({
samlProductID,
totpEmail,
}: inferSSRProps<typeof _getServerSideProps> & WithNonceProps) {
const searchParams = useSearchParams();
const { t } = useLocale();
const router = useRouter();
const formSchema = z
@@ -57,7 +58,7 @@ export default function Login({
.string()
.min(1, `${t("error_required_field")}`)
.email(`${t("enter_valid_email")}`),
password: z.string().min(1, `${t("error_required_field")}`),
password: !!totpEmail ? z.literal("") : z.string().min(1, `${t("error_required_field")}`),
})
// Passthrough other fields like totpCode
.passthrough();
@@ -77,7 +78,7 @@ export default function Login({
const telemetry = useTelemetry();
let callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "";
let callbackUrl = searchParams.get("callbackUrl") || "";
if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1);
@@ -169,7 +170,7 @@ export default function Login({
<EmailField
id="email"
label={t("email_address")}
defaultValue={totpEmail || (router.query.email as string)}
defaultValue={totpEmail || (searchParams?.get("email") as string)}
placeholder="john.doe@example.com"
required
{...register("email")}
+8 -2
View File
@@ -1,6 +1,6 @@
import type { GetServerSidePropsContext } from "next";
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { WEBSITE_URL } from "@calcom/lib/constants";
@@ -29,6 +29,12 @@ export function Logout(props: Props) {
}, [props.query?.survey]);
const { t } = useLocale();
const message = () => {
if (props.query?.passReset === "true") return "reset_your_password";
if (props.query?.emailChange === "true") return "email_change";
return "hope_to_see_you_soon";
};
return (
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
<div className="mb-4">
@@ -40,7 +46,7 @@ export function Logout(props: Props) {
{t("youve_been_logged_out")}
</h3>
<div className="mt-2">
<p className="text-subtle text-sm">{t("hope_to_see_you_soon")}</p>
<p className="text-subtle text-sm">{t(message())}</p>
</div>
</div>
</div>

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