Compare commits

..
151 Commits
Author SHA1 Message Date
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
466 changed files with 10143 additions and 5931 deletions
-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 }}
+28 -2
View File
@@ -15,6 +15,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:
@@ -27,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 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: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: app-store-results
path: test-results
+23 -2
View File
@@ -27,7 +27,6 @@ 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
@@ -35,10 +34,32 @@ jobs:
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: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: embed-react-results
path: test-results
+28 -2
View File
@@ -15,6 +15,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:
@@ -27,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 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: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: embed-core-results
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) }}
@@ -7,13 +7,8 @@ on:
- main
jobs:
env:
name: Create env file
uses: ./.github/workflows/env-create-file.yml
secrets: inherit
build:
name: Production build
needs: env
if: ${{ github.event_name == 'push' }}
uses: ./.github/workflows/production-build.yml
secrets: inherit
-7
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
@@ -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
+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 -7
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.
@@ -98,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)
@@ -128,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
@@ -254,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
```
@@ -545,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.
+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,
+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>
+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>
</>
);
}
-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 -470
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,116 +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) => ["calendar", "conferencing"].includes(category))) {
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",
@@ -25,6 +25,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import {
Alert,
Button,
CheckboxField,
Label,
@@ -32,9 +33,8 @@ import {
showToast,
TextField,
Tooltip,
Alert,
} from "@calcom/ui";
import { Edit, Copy } from "@calcom/ui/components/icon";
import { Copy, Edit } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -204,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}
@@ -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
@@ -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";
@@ -99,6 +101,7 @@ const CheckedHostField = ({
isFixed,
value,
onChange,
helperText,
...rest
}: {
labelText: string;
@@ -107,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">
@@ -136,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,
@@ -167,6 +184,7 @@ const RoundRobinHosts = ({
value={value}
placeholder={t("add_fixed_hosts")}
labelText={t("fixed_hosts")}
helperText={FixedHostHelper}
/>
<CheckedHostField
options={teamMembers.sort(sortByLabel)}
@@ -175,6 +193,7 @@ const RoundRobinHosts = ({
isFixed={false}
placeholder={t("add_attendees")}
labelText={t("round_robin_hosts")}
helperText={t("round_robin_helper")}
/>
</>
);
@@ -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 { 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) => {
@@ -310,7 +310,7 @@ function EventTypeSingleLayout({
showToast("Link copied!", "success");
}}
/>
<EmbedButton
<EventTypeEmbedButton
embedUrl={encodeURIComponent(embedLink)}
StartIcon={Code}
color="secondary"
@@ -450,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";
@@ -94,7 +94,12 @@ const DisableTwoFactorAuthModal = ({
<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>
@@ -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>;
+28 -23
View File
@@ -1,4 +1,5 @@
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";
@@ -6,8 +7,7 @@ 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";
@@ -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,7 +55,7 @@ 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;
@@ -76,6 +83,7 @@ const CustomI18nextProvider = (props: AppPropsWithoutNonce) => {
},
router: locale ? { locale } : props.router,
} as unknown as ComponentProps<typeof I18nextAdapter>;
return <I18nextAdapter {...passedProps} />;
};
@@ -89,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 && (
@@ -148,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;
})();
@@ -241,9 +246,8 @@ function OrgBrandProvider({ children }: { children: React.ReactNode }) {
}
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 = {
@@ -255,7 +259,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
const RemainingProviders = (
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<SessionProvider session={session || undefined}>
<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 */}
@@ -263,7 +267,8 @@ const AppProviders = (props: AppPropsWithChildren) => {
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>
@@ -276,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: [
"/((?!_next|.*avatar.png$|favicon.ico$).*)",
"/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*",
],
};
+13 -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",
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.1.5",
"version": "3.1.8",
"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",
+9 -10
View File
@@ -1,10 +1,10 @@
import type { GetStaticPropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
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";
@@ -22,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);
@@ -52,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) {
@@ -78,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 (
<>
+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>
+106 -50
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,34 +11,34 @@ 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 { 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, 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, 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;
@@ -45,9 +46,12 @@ export function UserPage(props: UserPageProps) {
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 || "";
/*
@@ -58,11 +62,20 @@ 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={nameOrUsername}
title={profile.name}
description={markdownStrippedBio}
meeting={{
title: markdownStrippedBio,
@@ -78,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")}
@@ -141,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>
@@ -195,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({
@@ -207,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,
@@ -222,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,
@@ -260,7 +310,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const [user] = users; //to be used when dealing with single user, not dynamic group
const profile = {
name: user.name || user.username,
name: user.name || user.username || "",
image: user.avatar,
theme: user.theme,
brandColor: user.brandColor,
@@ -280,25 +330,31 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}));
const isSingleUser = users.length === 1;
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: user.username,
user: {
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
},
eventTypes,
trpcState: ssr.dehydrate(),
isSingleUser,
markdownStrippedBio,
},
};
+16 -13
View File
@@ -6,6 +6,7 @@ import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import slugify from "@calcom/lib/slugify";
@@ -26,7 +27,7 @@ export default function Type({
away,
isBrandingHidden,
rescheduleUid,
org,
entity,
}: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
@@ -35,7 +36,7 @@ export default function Type({
eventSlug={slug}
rescheduleUid={rescheduleUid ?? undefined}
hideBranding={isBrandingHidden}
org={org}
entity={entity}
/>
<Booker
username={user}
@@ -43,7 +44,7 @@ export default function Type({
bookingData={booking}
isAway={away}
hideBranding={isBrandingHidden}
org={org}
entity={entity}
/>
</main>
);
@@ -58,7 +59,10 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
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: {
@@ -106,7 +110,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
return {
props: {
org,
entity: eventData.entity,
booking,
user: usernames.join("+"),
slug,
@@ -124,18 +128,17 @@ 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 { 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,
@@ -158,7 +161,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,
@@ -177,7 +180,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
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";
+3 -1
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;
@@ -49,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" });
}
}
@@ -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,
+2 -1
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" };
+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);
}
+4 -3
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
@@ -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>
+3 -7
View File
@@ -1,19 +1,15 @@
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
import PageWrapper from "@components/PageWrapper";
// To handle the IdP initiated login flow callback
export default function Page() {
const router = useRouter();
const searchParams = useSearchParams();
useEffect(() => {
if (!router.isReady) {
return;
}
const { code } = router.query;
const code = searchParams?.get("code");
signIn("saml-idp", {
callbackUrl: "/",
+14 -4
View File
@@ -1,5 +1,5 @@
import type { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
@@ -19,15 +19,25 @@ import EnterpriseLicense from "@components/setup/EnterpriseLicense";
import { ssrInit } from "@server/lib/ssr";
function useSetStep() {
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();
const setStep = (newStep = 1) => {
const _searchParams = new URLSearchParams(searchParams);
_searchParams.set("step", newStep.toString());
router.replace(`${pathname}?${_searchParams.toString()}`);
};
return setStep;
}
export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const router = useRouter();
const [value, setValue] = useState(props.isFreeLicense ? "FREE" : "EE");
const isFreeLicense = value === "FREE";
const [isEnabledEE, setIsEnabledEE] = useState(!props.isFreeLicense);
const setStep = (newStep: number) => {
router.replace(`/auth/setup?step=${newStep || 1}`, undefined, { shallow: true });
};
const setStep = useSetStep();
const steps: React.ComponentProps<typeof WizardForm>["steps"] = [
{
+3 -2
View File
@@ -1,6 +1,6 @@
import type { GetServerSidePropsContext } from "next";
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
@@ -27,11 +27,12 @@ import { ssrInit } from "@server/lib/ssr";
export type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Provider(props: SSOProviderPageProps) {
const searchParams = useSearchParams();
const router = useRouter();
useEffect(() => {
if (props.provider === "saml") {
const email = typeof router.query?.email === "string" ? router.query?.email : null;
const email = searchParams?.get("email");
if (!email) {
router.push("/auth/error?error=" + "Email not provided");
+1 -1
View File
@@ -1,5 +1,5 @@
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
+1 -1
View File
@@ -1,6 +1,6 @@
import { MailOpenIcon } from "lucide-react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { APP_NAME } from "@calcom/lib/constants";
+13 -17
View File
@@ -1,14 +1,14 @@
import { signIn } from "next-auth/react";
import Head from "next/head";
import { useRouter } from "next/router";
import * as React from "react";
import { useEffect, useState, useRef } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import z from "zod";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { trpc } from "@calcom/trpc/react";
import { Button, showToast } from "@calcom/ui";
import { Check, MailOpen, AlertTriangle } from "@calcom/ui/components/icon";
import { AlertTriangle, Check, MailOpen } from "@calcom/ui/components/icon";
import Loader from "@components/Loader";
import PageWrapper from "@components/PageWrapper";
@@ -54,8 +54,11 @@ const querySchema = z.object({
});
export default function Verify() {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const { t, sessionId, stripeCustomerId } = querySchema.parse(router.query);
const routerQuery = useRouterQuery();
const { t, sessionId, stripeCustomerId } = querySchema.parse(routerQuery);
const [secondsLeft, setSecondsLeft] = useState(30);
const { data } = trpc.viewer.public.stripeCheckoutSession.useQuery({
stripeCustomerId,
@@ -88,7 +91,7 @@ export default function Verify() {
}
}, [secondsLeft]);
if (!router.isReady || !data) {
if (!data) {
// Loading state
return <Loader />;
}
@@ -106,7 +109,7 @@ export default function Verify() {
<Head>
<title>
{/* @note: Ternary can look ugly ant his might be extracted later but I think at 3 it's not yet worth
it or too hard to read. */}
it or too hard to read. */}
{hasPaymentFailed
? "Your payment failed"
: sessionId
@@ -155,16 +158,9 @@ export default function Verify() {
e.preventDefault();
setSecondsLeft(30);
// Update query params with t:timestamp, shallow: true doesn't re-render the page
router.push(
router.asPath,
{
query: {
...router.query,
t: Date.now(),
},
},
{ shallow: true }
);
const _searchParams = new URLSearchParams(searchParams);
_searchParams.set("t", `${Date.now()}`);
router.replace(`${pathname}?${_searchParams.toString()}`);
return await sendVerificationLogin(customer.email, customer.username);
}}>
{secondsLeft > 0 ? `Resend in ${secondsLeft} seconds` : "Send another mail"}
+13 -21
View File
@@ -1,7 +1,6 @@
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
@@ -9,13 +8,20 @@ import Schedule from "@calcom/features/schedules/components/Schedule";
import Shell from "@calcom/features/shell/Shell";
import { availabilityAsString } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
import {
Button,
ConfirmationDialogContent,
Dialog,
DialogTrigger,
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuSeparator,
DropdownMenuTrigger,
Form,
Label,
showToast,
@@ -24,26 +30,14 @@ import {
Switch,
TimezoneSelect,
Tooltip,
Dialog,
DialogTrigger,
DropdownMenuSeparator,
Dropdown,
DropdownMenuContent,
DropdownItem,
DropdownMenuTrigger,
ConfirmationDialogContent,
VerticalDivider,
} from "@calcom/ui";
import { Info, Plus, Trash, MoreHorizontal } from "@calcom/ui/components/icon";
import { Info, MoreHorizontal, Plus, Trash } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import EditableHeading from "@components/ui/EditableHeading";
const querySchema = z.object({
schedule: z.coerce.number().positive().optional(),
});
type AvailabilityFormValues = {
name: string;
schedule: ScheduleType;
@@ -93,15 +87,13 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
};
export default function Availability() {
const searchParams = useSearchParams();
const { t, i18n } = useLocale();
const router = useRouter();
const utils = trpc.useContext();
const me = useMeQuery();
const {
data: { schedule: scheduleId },
} = useTypedQuery(querySchema);
const { fromEventType } = router.query;
const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
const fromEventType = searchParams?.get("fromEventType");
const { timeFormat } = me.data || { timeFormat: null };
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
+7 -4
View File
@@ -1,8 +1,9 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { getLayout } from "@calcom/features/MainLayout";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import Shell from "@calcom/features/shell/Shell";
import { ShellMain } from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import type { RouterOutputs } from "@calcom/trpc/react";
@@ -130,15 +131,17 @@ export default function AvailabilityPage() {
const { t } = useLocale();
return (
<div>
<Shell
<ShellMain
heading={t("availability")}
hideHeadingOnMobile
subtitle={t("configure_availability")}
CTA={<NewScheduleButton />}>
<WithQuery success={({ data }) => <AvailabilityList {...data} />} customLoader={<SkeletonLoader />} />
</Shell>
</ShellMain>
</div>
);
}
AvailabilityPage.getLayout = getLayout;
AvailabilityPage.PageWrapper = PageWrapper;
+23 -22
View File
@@ -4,7 +4,7 @@ import { createEvent } from "ics";
import type { GetServerSidePropsContext } from "next";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { RRule } from "rrule";
import { z } from "zod";
@@ -39,6 +39,7 @@ import {
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
@@ -47,11 +48,9 @@ import { localStorage } from "@calcom/lib/webstorage";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Button, EmailInput, HeadSeo, Badge, useCalcomTheme, Alert } from "@calcom/ui";
import { X, ExternalLink, ChevronLeft, Check, Calendar } from "@calcom/ui/components/icon";
import { AlertCircle } from "@calcom/ui/components/icon";
import { bookingMetadataSchema, customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Alert, Badge, Button, EmailInput, HeadSeo, useCalcomTheme } from "@calcom/ui";
import { AlertCircle, Calendar, Check, ChevronLeft, ExternalLink, X } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -99,6 +98,9 @@ const querySchema = z.object({
export default function Success(props: SuccessProps) {
const { t } = useLocale();
const router = useRouter();
const routerQuery = useRouterQuery();
const pathname = usePathname();
const searchParams = useSearchParams();
const {
allRemainingBookings,
isSuccessBookingPage,
@@ -106,7 +108,7 @@ export default function Success(props: SuccessProps) {
formerTime,
email,
seatReferenceUid,
} = querySchema.parse(router.query);
} = querySchema.parse(routerQuery);
const attendeeTimeZone = props?.bookingInfo?.attendees.find(
(attendee) => attendee.email === email
@@ -145,30 +147,27 @@ export default function Success(props: SuccessProps) {
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
function setIsCancellationMode(value: boolean) {
const query_ = { ...router.query };
const _searchParams = new URLSearchParams(searchParams);
if (value) {
query_.cancel = "true";
_searchParams.set("cancel", "true");
} else {
if (query_.cancel) {
delete query_.cancel;
if (_searchParams.get("cancel")) {
_searchParams.delete("cancel");
}
}
router.replace(
{
pathname: router.pathname,
query: { ...query_ },
},
undefined,
{ scroll: false }
);
router.replace(`${pathname}?${_searchParams.toString()}`);
}
let evtName = props.eventType.eventName;
if (eventType.isDynamic && bookingInfo.responses?.title) {
evtName = bookingInfo.responses.title as string;
}
const eventNameObject = {
attendeeName,
eventType: props.eventType.title,
eventName: (props.dynamicEventName as string) || props.eventType.eventName,
eventName: evtName,
host: props.profile.name || "Nameless",
location: location,
bookingFields: bookingInfo.responses,
@@ -405,7 +404,9 @@ export default function Success(props: SuccessProps) {
</>
)}
<div className="font-medium">{t("what")}</div>
<div className="col-span-2 mb-6 last:mb-0">{eventName}</div>
<div className="col-span-2 mb-6 last:mb-0" data-testid="booking-title">
{eventName}
</div>
<div className="font-medium">{t("when")}</div>
<div className="col-span-2 mb-6 last:mb-0">
{reschedule && !!formerTime && (
@@ -440,7 +441,7 @@ export default function Success(props: SuccessProps) {
{bookingInfo?.user && (
<div className="mb-3">
<div>
<span data-testid={`host-name-${bookingInfo.user.name}`} className="mr-2">
<span data-testid="booking-host-name" className="mr-2">
{bookingInfo.user.name}
</span>
<Badge variant="blue">{t("Host")}</Badge>
+117 -79
View File
@@ -1,16 +1,21 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { GetStaticPaths, GetStaticProps } from "next";
import { useRouter } from "next/router";
import { Fragment } from "react";
import React from "react";
import { z } from "zod";
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
import BookingLayout from "@calcom/features/bookings/layout/BookingLayout";
import { getLayout } from "@calcom/features/MainLayout";
import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import { ShellMain } from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { HorizontalTabs } from "@calcom/ui";
import type { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui";
import { Alert, Button, EmptyScreen } from "@calcom/ui";
import { Calendar } from "@calcom/ui/components/icon";
@@ -32,6 +37,28 @@ type RecurringInfo = {
bookings: { [key: string]: Date[] };
};
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{
name: "upcoming",
href: "/bookings/upcoming",
},
{
name: "unconfirmed",
href: "/bookings/unconfirmed",
},
{
name: "recurring",
href: "/bookings/recurring",
},
{
name: "past",
href: "/bookings/past",
},
{
name: "cancelled",
href: "/bookings/cancelled",
},
];
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
const descriptionByStatus: Record<NonNullable<BookingListingStatus>, string> = {
@@ -47,9 +74,9 @@ const querySchema = z.object({
});
export default function Bookings() {
const params = useParamsWithFallback();
const { data: filterQuery } = useFilterQuery();
const router = useRouter();
const { status } = router.isReady ? querySchema.parse(router.query) : { status: "upcoming" as const };
const { status } = params ? querySchema.parse(params) : { status: "upcoming" as const };
const { t } = useLocale();
const query = trpc.viewer.bookings.get.useInfiniteQuery(
@@ -62,7 +89,7 @@ export default function Bookings() {
},
{
// first render has status `undefined`
enabled: router.isReady,
enabled: true,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
@@ -112,90 +139,101 @@ export default function Bookings() {
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
return (
<BookingLayout heading={t("bookings")} subtitle={t("bookings_description")}>
<div className="flex w-full flex-col" ref={animationParentRef}>
{query.status === "error" && (
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
)}
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
{query.status === "success" && !isEmpty && (
<>
{!!bookingsToday.length && status === "upcoming" && (
<div className="mb-6 pt-2 xl:pt-0">
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
<p className="text-subtle mb-2 text-xs font-medium uppercase leading-4">{t("today")}</p>
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="today-bookings">
<Fragment>
{bookingsToday.map((booking: BookingOutput) => (
<BookingListItem
key={booking.id}
listingStatus={status}
recurringInfo={recurringInfoToday}
{...booking}
/>
<ShellMain hideHeadingOnMobile heading={t("bookings")} subtitle={t("bookings_description")}>
<div className="flex flex-col">
<div className="flex flex-col flex-wrap lg:flex-row">
<HorizontalTabs tabs={tabs} />
<div className="max-w-full overflow-x-auto xl:ml-auto">
<FiltersContainer />
</div>
</div>
<main className="w-full">
<div className="flex w-full flex-col" ref={animationParentRef}>
{query.status === "error" && (
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
)}
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
{query.status === "success" && !isEmpty && (
<>
{!!bookingsToday.length && status === "upcoming" && (
<div className="mb-6 pt-2 xl:pt-0">
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
<p className="text-subtle mb-2 text-xs font-medium uppercase leading-4">{t("today")}</p>
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="today-bookings">
<Fragment>
{bookingsToday.map((booking: BookingOutput) => (
<BookingListItem
key={booking.id}
listingStatus={status}
recurringInfo={recurringInfoToday}
{...booking}
/>
))}
</Fragment>
</tbody>
</table>
</div>
</div>
)}
<div className="pt-2 xl:pt-0">
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
const recurringInfo = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
<BookingListItem
key={booking.id}
listingStatus={status}
recurringInfo={recurringInfo}
{...booking}
/>
);
})}
</Fragment>
))}
</Fragment>
</tbody>
</table>
</tbody>
</table>
</div>
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
</>
)}
{query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon={Calendar}
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(),
description: t(descriptionByStatus[status]),
})}
/>
</div>
)}
<div className="pt-2 xl:pt-0">
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
const recurringInfo = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
<BookingListItem
key={booking.id}
listingStatus={status}
recurringInfo={recurringInfo}
{...booking}
/>
);
})}
</Fragment>
))}
</tbody>
</table>
</div>
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
</>
)}
{query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon={Calendar}
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(),
description: t(descriptionByStatus[status]),
})}
/>
</div>
)}
</main>
</div>
</BookingLayout>
</ShellMain>
);
}
Bookings.PageWrapper = PageWrapper;
Bookings.getLayout = getLayout;
export const getStaticProps: GetStaticProps = async (ctx) => {
const params = querySchema.safeParse(ctx.params);
+4 -4
View File
@@ -25,7 +25,7 @@ export default function Type({
away,
isBrandingHidden,
isTeamEvent,
org,
entity,
}: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
@@ -34,7 +34,7 @@ export default function Type({
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
org={org}
entity={entity}
/>
<Booker
username={user}
@@ -43,7 +43,7 @@ export default function Type({
isAway={away}
hideBranding={isBrandingHidden}
isTeamEvent={isTeamEvent}
org={org}
entity={entity}
/>
</main>
);
@@ -132,7 +132,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
return {
props: {
org,
entity: eventData.entity,
booking,
away: user?.away,
user: username,
@@ -87,6 +87,7 @@ export type FormValues = {
description: string;
disableGuests: boolean;
requiresConfirmation: boolean;
requiresBookerEmailVerification: boolean;
recurringEvent: RecurringEvent | null;
schedulingType: SchedulingType | null;
hidden: boolean;
@@ -102,6 +103,7 @@ export type FormValues = {
phone?: string;
hostDefault?: string;
credentialId?: number;
teamName?: string;
}[];
customInputs: CustomInputParsed[];
schedule: number | null;
@@ -205,6 +207,10 @@ const EventTypePage = (props: EventTypeSetupProps) => {
message = `${err.data.code}: ${t(err.message)}`;
}
if (err.data?.code === "INTERNAL_SERVER_ERROR") {
message = t("unexpected_error_try_again");
}
showToast(message ? t(message) : t(err.message), "error");
},
});
@@ -247,10 +253,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
durationLimits: eventType.durationLimits || undefined,
length: eventType.length,
hidden: eventType.hidden,
hashedLink: eventType.hashedLink?.link || undefined,
periodDates: {
startDate: periodDates.startDate,
endDate: periodDates.endDate,
},
offsetStart: eventType.offsetStart,
bookingFields: eventType.bookingFields,
periodType: eventType.periodType,
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
@@ -526,6 +534,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
const { data } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
if (!data) return null;
return <EventTypePage {...(data as EventTypeSetupProps)} />;
};
+81 -72
View File
@@ -2,35 +2,40 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { User } from "@prisma/client";
import { Trans } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import type { FC } from "react";
import { useEffect, useState, memo } from "react";
import { memo, useEffect, useState } from "react";
import { z } from "zod";
import { getLayout } from "@calcom/features/MainLayout";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import Shell from "@calcom/features/shell/Shell";
import { ShellMain } from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { SchedulingType } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc, TRPCClientError } from "@calcom/trpc/react";
import {
Alert,
Avatar,
AvatarGroup,
Badge,
Button,
ButtonGroup,
ConfirmationDialogContent,
CreateButton,
Dialog,
Dropdown,
DropdownItem,
@@ -40,15 +45,13 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
EmptyScreen,
HeadSeo,
HorizontalTabs,
Label,
showToast,
Skeleton,
Switch,
Tooltip,
CreateButton,
HorizontalTabs,
HeadSeo,
Skeleton,
Label,
Alert,
} from "@calcom/ui";
import {
ArrowDown,
@@ -63,18 +66,18 @@ import {
MoreHorizontal,
Trash,
Upload,
Users,
User as UserIcon,
Users,
} from "@calcom/ui/components/icon";
import useMeQuery from "@lib/hooks/useMeQuery";
import { EmbedButton, EmbedDialog } from "@components/Embed";
import PageWrapper from "@components/PageWrapper";
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
type GetByViewerResponse = RouterOutputs["viewer"]["eventTypes"]["getByViewer"] | undefined;
interface EventTypeListHeadingProps {
profile: EventTypeGroupProfile;
@@ -201,6 +204,8 @@ const MemoizedItem = memo(Item);
export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => {
const { t } = useLocale();
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const orgBranding = useOrgBranding();
const [parent] = useAutoAnimate<HTMLUListElement>();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -291,25 +296,19 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
// inject selection data into url for correct router history
const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => {
const query = {
...router.query,
dialog: "duplicate",
title: eventType.title,
description: eventType.description,
slug: eventType.slug,
id: eventType.id,
length: eventType.length,
pageSlug: group.profile.slug,
};
router.push(
{
pathname: router.pathname,
query,
},
undefined,
{ shallow: true }
);
const newSearchParams = new URLSearchParams(searchParams);
function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) {
if (value) newSearchParams.set(key, value.toString());
if (value === null) newSearchParams.delete(key);
}
setParamsIfDefined("dialog", "duplicate");
setParamsIfDefined("title", eventType.title);
setParamsIfDefined("description", eventType.description);
setParamsIfDefined("slug", eventType.slug);
setParamsIfDefined("id", eventType.id);
setParamsIfDefined("length", eventType.length);
setParamsIfDefined("pageSlug", group.profile.slug);
router.push(`${pathname}?${newSearchParams.toString()}`);
};
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
@@ -509,7 +508,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
)}
{!isManagedEventType && (
<DropdownMenuItem className="outline-none">
<EmbedButton
<EventTypeEmbedButton
as={DropdownItem}
type="button"
StartIcon={Code}
@@ -517,7 +516,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
embedUrl={encodeURIComponent(embedLink)}
eventId={type.id}>
{t("embed")}
</EmbedButton>
</EventTypeEmbedButton>
</DropdownMenuItem>
)}
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
@@ -717,10 +716,7 @@ const EventTypeListHeading = ({
<Avatar
alt={profile?.name || ""}
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
imageSrc={
`${orgBranding?.fullDomain ?? WEBAPP_URL}/${teamId ? "team/" : ""}${profile.slug}/avatar.png` ||
undefined
}
imageSrc={`${orgBranding?.fullDomain ?? WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
size="md"
className="mt-1 inline-flex justify-center"
/>
@@ -769,14 +765,12 @@ const CreateFirstEventTypeView = () => {
);
};
const CTA = () => {
const CTA = ({ data }: { data: GetByViewerResponse }) => {
const { t } = useLocale();
const query = trpc.viewer.eventTypes.getByViewer.useQuery();
if (!data) return null;
if (!query.data) return null;
const profileOptions = query.data.profiles
const profileOptions = data.profiles
.filter((profile) => !profile.readOnly)
.map((profile) => {
return {
@@ -834,25 +828,29 @@ const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
);
};
const Main = () => {
const Main = ({
status,
error,
data,
filters,
}: {
status: string;
data: GetByViewerResponse;
error: any;
filters: ReturnType<typeof getTeamsFiltersFromQuery>;
}) => {
const isMobile = useMediaQuery("(max-width: 768px)");
const router = useRouter();
const filters = getTeamsFiltersFromQuery(router.query);
const searchParams = useSearchParams();
const orgBranding = useOrgBranding();
// TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
const res = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters });
if (res.status === "loading") {
if (!data || status === "loading") {
return <SkeletonLoader />;
}
if (res.status === "error") {
return <Alert severity="error" title="Something went wrong" message={res.error.message} />;
if (status === "error") {
return <Alert severity="error" title="Something went wrong" message={error.message} />;
}
const data = res.data;
const isFilteredByOnlyOneItem =
(filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) && data.eventTypeGroups.length === 1;
return (
@@ -892,20 +890,28 @@ const Main = () => {
)
)}
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView />}
<EmbedDialog />
{router.query.dialog === "duplicate" && <DuplicateDialog />}
<EventTypeEmbedDialog />
{searchParams?.get("dialog") === "duplicate" && <DuplicateDialog />}
</>
);
};
const EventTypesPage = () => {
const { t } = useLocale();
const router = useRouter();
const searchParams = useSearchParams();
const { open } = useIntercom();
const { query } = router;
const { data: user } = useMeQuery();
const [showProfileBanner, setShowProfileBanner] = useState(false);
const orgBranding = useOrgBranding();
const routerQuery = useRouterQuery();
const filters = getTeamsFiltersFromQuery(routerQuery);
// TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
const { data, status, error } = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters }, {
refetchOnWindowFocus: false,
cacheTime: 1 * 60 * 60 * 1000,
staleTime: 1 * 60 * 60 * 1000,
});
function closeBanner() {
setShowProfileBanner(false);
@@ -914,35 +920,38 @@ const EventTypesPage = () => {
}
useEffect(() => {
if (query?.openIntercom && query?.openIntercom === "true") {
if (searchParams?.get("openIntercom") === "true") {
open();
}
setShowProfileBanner(
!!orgBranding && !document.cookie.includes("calcom-profile-banner=1") && !user?.completedOnboarding
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setShowProfileBanner(
!!orgBranding && !document.cookie.includes("calcom-profile-banner=1") && !user?.completedOnboarding
);
}, [orgBranding, user]);
return (
<div>
<ShellMain
withoutSeo
heading={t("event_types_page_title")}
hideHeadingOnMobile
subtitle={t("event_types_page_subtitle")}
afterHeading={showProfileBanner && <SetupProfileBanner closeAction={closeBanner} />}
beforeCTAactions={<Actions />}
CTA={<CTA data={data} />}>
<HeadSeo
title="Event Types"
description="Create events to share for people to book on your calendar."
/>
<Shell
withoutSeo
heading={t("event_types_page_title")}
hideHeadingOnMobile
subtitle={t("event_types_page_subtitle")}
afterHeading={showProfileBanner && <SetupProfileBanner closeAction={closeBanner} />}
beforeCTAactions={<Actions />}
CTA={<CTA />}>
<Main />
</Shell>
</div>
<Main data={data} status={status} error={error} filters={filters} />
</ShellMain>
);
};
EventTypesPage.getLayout = getLayout;
EventTypesPage.PageWrapper = PageWrapper;
export default EventTypesPage;
+9 -12
View File
@@ -1,7 +1,7 @@
import type { GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Head from "next/head";
import { useRouter } from "next/router";
import { usePathname, useRouter } from "next/navigation";
import type { CSSProperties } from "react";
import { Suspense } from "react";
import { z } from "zod";
@@ -9,6 +9,7 @@ import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import prisma from "@calcom/prisma";
import { trpc } from "@calcom/trpc";
import { Button, StepCard, Steps } from "@calcom/ui";
@@ -47,11 +48,12 @@ const stepRouteSchema = z.object({
// TODO: Refactor how steps work to be contained in one array/object. Currently we have steps,initalsteps,headers etc. These can all be in one place
const OnboardingPage = () => {
const pathname = usePathname();
const params = useParamsWithFallback();
const router = useRouter();
const [user] = trpc.viewer.me.useSuspenseQuery();
const { t } = useLocale();
const result = stepRouteSchema.safeParse(router.query);
const result = stepRouteSchema.safeParse(params);
const currentStep = result.success ? result.data.step[0] : INITIAL_STEP;
const from = result.success ? result.data.from : "";
@@ -96,12 +98,7 @@ const OnboardingPage = () => {
const goToIndex = (index: number) => {
const newStep = steps[index];
router.push(
{
pathname: `/getting-started/${stepTransform(newStep)}`,
},
undefined
);
router.push(`/getting-started/${stepTransform(newStep)}`);
};
const currentStepIndex = steps.indexOf(currentStep);
@@ -118,16 +115,16 @@ const OnboardingPage = () => {
"--cal-brand-subtle": "#9CA3AF",
} as CSSProperties
}
key={router.asPath}>
key={pathname}>
<Head>
<title>{`${APP_NAME} - ${t("getting_started")}`}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="mx-auto px-4 py-6 md:py-24">
<div className="mx-auto py-6 sm:px-4 md:py-24">
<div className="relative">
<div className="sm:mx-auto sm:w-full sm:max-w-[600px]">
<div className="mx-auto sm:max-w-[520px]">
<div className="mx-auto px-4 sm:max-w-[520px]">
<header>
<p className="font-cal mb-3 text-[28px] font-medium leading-7">
{headers[currentStepIndex]?.title || "Undefined title"}
+5 -3
View File
@@ -1,3 +1,4 @@
import { getLayout } from "@calcom/features/MainLayout";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import {
AverageEventDurationChart,
@@ -9,7 +10,7 @@ import {
} from "@calcom/features/insights/components";
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
import { Filters } from "@calcom/features/insights/filters";
import Shell from "@calcom/features/shell/Shell";
import { ShellMain } from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -56,7 +57,7 @@ export default function InsightsPage() {
return (
<div>
<Shell hideHeadingOnMobile>
<ShellMain hideHeadingOnMobile>
<UpgradeTip
title={t("make_informed_decisions")}
description={t("make_informed_decisions_description")}
@@ -111,12 +112,13 @@ export default function InsightsPage() {
</FiltersProvider>
)}
</UpgradeTip>
</Shell>
</ShellMain>
</div>
);
}
InsightsPage.PageWrapper = PageWrapper;
InsightsPage.getLayout = getLayout;
// If feature flag is disabled, return not found on getServerSideProps
export const getServerSideProps = async () => {
@@ -1,13 +1,14 @@
import type { GetServerSidePropsContext } from "next";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import prisma from "@calcom/prisma";
import PageWrapper from "@components/PageWrapper";
import type { PageProps as UserTypePageProps } from "../../../[user]/[type]";
import UserTypePage, { getServerSideProps as GSSUserTypePage } from "../../../[user]/[type]";
import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../team/[slug]/[type]";
import type { PageProps as TeamTypePageProps } from "../../../team/[slug]/[type]";
import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../team/[slug]/[type]";
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const team = await prisma.team.findFirst({
@@ -16,9 +17,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
parentId: {
not: null,
},
parent: {
slug: ctx.query.orgSlug as string,
},
parent: getSlugOrRequestedSlug(ctx.query.orgSlug as string),
},
select: {
id: true,
@@ -1,5 +1,6 @@
import type { GetServerSidePropsContext } from "next";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import prisma from "@calcom/prisma";
import PageWrapper from "@components/PageWrapper";
@@ -16,9 +17,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
parentId: {
not: null,
},
parent: {
slug: ctx.query.orgSlug as string,
},
parent: getSlugOrRequestedSlug(ctx.query.orgSlug as string),
},
select: {
id: true,

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