Compare commits

...
96 Commits
Author SHA1 Message Date
Alex van Andel 6cf53feaf8 v2.5.7
release.yml / release (push) Canceled after 0s
2023-02-02 01:06:20 +00:00
Leo Giovanetti 376b693d91 Not allowing random duration (#6743) 2023-02-01 22:19:37 +00:00
Alex van Andel 831aa72d23 Invalidate hasTeamPlan when the last team is removed (#6837) 2023-02-01 21:23:34 +00:00
Peer RichelsenandAlex van Andel ef78db7856 /teams improvements (#6831)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-02-01 20:41:47 +00:00
Nafees Nazik 7ee5967075 feat: add theming options to teams (#6807) 2023-02-01 19:27:26 +01:00
Alex van Andel 5b165e6ebf Fixed broken path to fix SB build failure 2023-02-01 16:09:45 +00:00
Joe Shajan 624f52c305 webhook from ui design change (#6803)
* webhook from ui change

* fix: webhook test border & test webhook font style
2023-02-01 14:51:42 +00:00
GitStart-Cal.comandgitstart-calcom df25ae6a3d [CAL-845] Improve the custom event name modal UI/UX (#6660)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-02-01 13:10:16 +01:00
Josh Avant 7f65d7f3c3 Removes pay_later_instructions (#6824) 2023-02-01 09:17:11 +01:00
Alex van AndelandHariom Balhara c42674595c Add X-Frame-Options to page headers (#6784)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-02-01 11:28:47 +05:30
Nafees Nazikandalannnc 17eb45b0b6 fix: input component style in routing forms (#6731)
* fix: remove custom className

* removed extra type text

removed extra type text from input

---------

Co-authored-by: alannnc <alannnc@gmail.com>
2023-02-01 01:39:29 +00:00
Peer Richelsen 48d0edeaf4 fixed event-type description 2023-01-31 23:35:05 +01:00
Peer Richelsen ed4b31767f lint 2023-01-31 23:33:20 +01:00
Peer Richelsen cebb10bfe4 re-adding subtitle to Shell 2023-01-31 23:31:04 +01:00
Ben Lam 7a6e7c2f1d Removed unused variables (#6704) (#6750) 2023-01-31 22:29:24 +00:00
Peer Richelsen 0ffed24359 user management moved to console.cal.com (#6799) 2023-01-31 21:59:11 +00:00
Nafees NazikandAlan e121615d36 fix: installed apps and admin apps layout spacing issues. (#6753)
* feat: add highlight props

* feat: add isTemplate prop

* feat: add invalid credintial prop

* feat: add children prop

* feat: render app list card

* fix: add horizontal gap

* fix: installed app layout style

* fix: admin layout

* feat: add isGlobal to returned data

* feat: fix admin tab.

* use common components list and AppListCard

* show isDefault Badge

* hide switch for global apps

* fix: show switch

* fix: remove isglobal

* fix: layout

* refactor: remove unused component and import

* feat: use app list card component

* fix: query param

* fix: prevent unnecessary props passed to the component

* feat: add disabled style

* feat: add disconnect integration modal

* feat: new changes.

* render disconnect integration model only once.

* add dropdown for actions

* feat: changes in admin apps.

* use dropdown for selecting action buttons.

* use modal for editing keys.

* Remove boolean comparison

---------

Co-authored-by: Alan <alannnc@gmail.com>
2023-01-31 22:45:38 +01:00
4f9aa8bd96 Hide attendees from calendar description (#6800)
* Show the organizer timezone when booking on seats

* Only show organizer in event description

* Type fixes

* Update snapshot with new response

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Alan <alannnc@gmail.com>
2023-01-31 14:14:19 -07:00
alannncandsean-brydon 64ede37ad6 Fix embed modal leading inputs (#6822)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-01-31 13:58:15 -07:00
36b6c351ec Session Timeout (#6439)
* Implementation

* Unifying UI to password screen

* Tweaks for session in password section

* Update apps/web/pages/settings/security/password.tsx

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

* Update packages/features/kbar/Kbar.tsx

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

* Update packages/features/settings/layouts/SettingsLayout.tsx

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

* Relying on extra db query

* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* Adds deployment settings to DB (#6706)

* WIP

* Adds DeploymentTheme

* Add missing migrations

* Adds client extensions for deployment

* Cleanup

* Using same version as other deps

* Reverting prisma changes and fixing things

* Uneeded tx-expect-error

* Fixing default value

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

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-01-31 17:44:14 -03:00
GitStart-Cal.comandgitstart-calcom 9ab5a3149c [CAL-807] /event-types/single - Fix the header (#6738)
release.yml / release (push) Canceled after 0s
* [CAL-807] /event-types/single - Fix the header

* [CAL-807] /event-types/single - Fix the header

* [CAL-807] /event-types/single - Fix the header

---------

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-01-31 20:19:02 +00:00
Nafees NazikandAlan 3b8f436bdf feat: tabs for teams in mobile (#6685)
* fix: padding

* feat: add teams tab component

* fix: add image to type

* fix: current path check

* fix: add avatar prop

* feat: add teams tab for mobile

* fix: padding

* fix: background and padding x

* fix: empty logic

* fix: conditions

* fix: bg

---------

Co-authored-by: Alan <alannnc@gmail.com>
2023-01-31 12:54:40 -07:00
Nafees Nazik 77d45a6ec2 fix: use guest email in create booking link banner (#6694) 2023-01-31 19:47:32 +00:00
Carina WollendorferandCarinaWolli 50c74cf286 change useHasTeamPlan hook to useHasPaidPlan (#6699)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-31 19:10:21 +00:00
Richard Poelderl dcbafced8b update isCalcom for canonicals to handle https://cal.com (#6802)
* update isCalcom for canonicals to handle https://cal.com

* avoid env var

* prettier

* handle window undefined

* handle window undefined during pre-render

* add some logs

* add some logs

* Revert "add some logs"

This reverts commit 9d27de029024e526417bc79e9cee42d7333ac592.
2023-01-31 19:07:50 +00:00
GitStart-Cal.comandgitstart-calcom 4679c80ea0 [CAL-727] [500] on /api/trpc/[trpc] (#6795)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-01-31 18:50:06 +00:00
Bram Verstraten 291316ee01 Fix Dutch custom event names (#6806)
Continued from #6375
2023-01-31 18:46:07 +00:00
sean-brydon 208fc2f0e4 Feat - invite user to team flow improvements (#6725) 2023-01-31 18:16:43 +00:00
Joe Au-Yeung 99e71054ff Change lint action to run on BuildJet (#6819) 2023-01-31 19:08:08 +01:00
Nafees Nazik 9d118d4e34 fix: flash of i18n in teams page (#6818) 2023-01-31 19:05:18 +01:00
+6 d1a877e70d Add additional guests (#6419)
* Add additional guests

* WIP

* WIP

* Type fix

* WIP

* Add validation for already invited attendees

* Revert type to email

* remove controlled fields

* disables signups via env variable (#6212)

* disables signups via env variable

* Apply suggestions from code review

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

* Reusable Upgrade component for teams only features (#6473)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* i18n udpate

* fix: darkmode in booking/[id] page (#6458)

* fix: text-area style

* fix: border styles

* fix: german translation for reschedule booking email  subject (#6483)

* fix: german translation for reschedule subject

* fix: add a full stop

* fix: app store spacing issues (#6481)

* feat: add equal spacing with gap

* fix: title style

* fix: title style

* fix: remove margin

* fix: remove margin

* fix: sub header spacing and slider button

* fix: category card text

* fix: app store spacing issues

* fix: tab in tablet

* fix: shell

* fix: styling of dropdown component (#6440)

* fix: styling of dropdown component

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

* Apply suggestions from code review

* fix: use dropdown item in all pages

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

* chore: undo

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>

* readding sidebar tips for everyone, added 3 more features to teams empty screen (#6475)

* readding sidebar tips for everyone

* added 3 more features to teams plan

* responsive fixes

* responsive fixes

* small responsive fix

* CTA alignment fixed

* Minor alignment fixes to Shell for event-types/[type]

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

* fix: wrong i18n  placeholder in many locales (#6496)

* fix: czech translation

* fix: spanish translation

* fix: italian translation

* fix: japanese translation

* fix: korean translation

* fix: dutch translation

* fix: norwegian translation

* fix: polish translation

* fix: portuguese translation

* fix: portuguese translation

* fix: romanian translation

* fix: russian translation

* fix: serbian translation

* fix: swedish translation

* fix: turkish translation

* fix: ukrainian translation

* fix: vietnamese translation

* fix: chinese simplified translation

* fix: chinese traditional translation

* Disable redirect for cancellation page (#6498)

* [CAL-677] /event-type : Event Setup sidebar item doesn't reflect multiple durations (#6390)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* Added fallback to hostsFixed too which is used in COLLECTIVE (#6500)

* Don't update the field when changing the select (#6486)

* Don't update the field when changing the select

* Fixing missing label

* Applied the correct y-8 vertical margin

* Changed the description vertical offset from title a bit more

* Add Trash icon on Availability list dropdown

* fix: tablet sidebar profile being too wide and not centered  (#6490)

* feat: updated app list items styling issues inside settings/conferencing

* feat: fixed tablet sidebar width and not centered issue

* fix: removed un-related changes from this pr

* deleted .env

* fix: added the accidentaly deleted .env file

* fix: removed unwanted .env file from the pr

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* Bring back PublicShell (#6505)

* Add red text to error message

* WIP

* Rearrange guest input

* Fix validate unique guests logic

* Allow guests on reschedule

* Lint fix

* More lint fixes

* Lint fixes

* More lint fixes

* Fix remove guest button margin

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Jeff Loiselle <jeff@loiselles.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Ronit Panda <72537293+rtpa25@users.noreply.github.com>
Co-authored-by: Alan <alannnc@gmail.com>
2023-01-31 17:36:38 +00:00
Nafees Nazik 238b509be6 fix: border color in dark teams page (#6817) 2023-01-31 18:21:51 +01:00
Richard Poelderl c7471b404c fix(web): remove noindex & nofollow tags from the App and Shell components (#6816)
Fixes https://github.com/calcom/cal.com/issues/6815
2023-01-31 17:08:59 +00:00
Alex van Andel 4db4df8ace Correct invalid ukraine language code ua -> uk 2023-01-30 21:09:09 +00:00
Udit Takkar a15b8fb92e fix: booking limits saved as undefined (#6720)
* fix: booking limits saved as undefined

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

* fix: add event limit

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-30 19:38:22 +01:00
Syed Ali Shahbaz a4bed48eaf Standardize response codes in Login (#6787)
* standardizing login response codes

* playwright fix

* added translation
2023-01-30 18:37:03 +00:00
Peer Richelsen 1be6f192d4 added impersonation tip (#6794) 2023-01-30 16:19:11 +01:00
Richard Poelderl f1c5838aef fix(web): set canonical as https://cal.com in the <HeadSeo/> (#6693)
* fix(web): set canonical as https://cal.com in the <HeadSeo/>

* use `window.document.location?.href` if self hosted

* clean up

* use `isCalcom` from library

* handle trailing slash on homepage again
2023-01-30 13:54:07 +01:00
Peer Richelsen 8e994dcc33 adding ukrainian (#6791) 2023-01-30 13:49:08 +01:00
Peer Richelsen 506bbe3d31 Update README.md 2023-01-30 13:48:02 +01:00
Nafees Nazik 419a84c5a2 fix: location select menu overflowing the container (#6719) 2023-01-30 12:58:37 +01:00
Hariom Balhara 8cf0050d1f Fix: EventType Duplication (#6778) 2023-01-30 12:55:08 +01:00
Sai Deepesh 6c23813192 fix padding of lenght input on add event modal (#6771) 2023-01-30 11:12:53 +00:00
Joe Au-Yeung 875f922597 Disable autocomplete on password field (#6705) 2023-01-30 09:42:52 +00:00
Udit Takkar c8e3f6494a fix: back button and heading size (#6768)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-29 10:38:31 +01:00
Carina WollendorferandCarinaWolli 333801d2ae Fix shift-enter in bio editor (#6769)
* fix wrong enabled update button at first load

* fix shift-enter error

* small fixes + code clean up

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-27 23:14:58 +00:00
Joe Au-Yeung 60f7528cbc On seats, hide attendees if option is selected (#6767)
* On seats, hide attendees if option is selected

* Remove unused line
2023-01-27 12:10:49 -07:00
Peer Richelsen 0faf294367 removed credits from auth container (#6766) 2023-01-27 19:20:39 +01:00
Peer Richelsen 87c178e53b added blue color for links in bio (#6760) 2023-01-27 17:03:04 +01:00
Peer Richelsen 7710b1b718 Update README.md 2023-01-27 15:14:16 +01:00
Peer Richelsen d71c6ccb31 Update README.md 2023-01-27 12:07:12 +01:00
GitStart-Cal.comandgitstart-calcom 33a4117fb5 On duplicating an event type, spacing between arrows and label is wide (Length field) (#6717)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-01-27 11:06:29 +01:00
Omar López a90419c40b Adds booking data to SDK success event (#6737) 2023-01-27 11:02:33 +05:30
Omar López 16adf86c8c Upgrades typescript to 4.9.4 (#6747)
* Upgrades typescript to 4.9.4

* Type fixes
2023-01-26 18:50:56 -07:00
Peer Richelsen 3b838c475a replaced edit location dialog subtitle with link to app store (#6746) 2023-01-27 00:33:56 +01:00
Omar López cf33106d8b WIP: SIU: Maximum call stack size exceeded fix? (#6740)
* Maximum call stack size exceeded fix?

# Conflicts:
#	packages/prisma/index.ts

* Prisma client extensions is too much for our TS

* Removed unused Icons

* Type fixes
2023-01-26 17:51:03 -05:00
zomars a63f9ddf61 Adds deployment settings to DB (#6706) 2023-01-26 13:36:15 -07:00
zomars 1cf83068b2 Update yarn.lock 2023-01-26 13:33:59 -07:00
Hariom Balhara 23de92f818 Hotfix: ping,riverside,whereby and around not showing up in list (#6712) 2023-01-26 13:31:13 -07:00
zomars c6fc1f756b Update yarn.lock 2023-01-25 16:56:44 -07:00
zomars 88b7c506f1 Include submodule dependencies in lockfile 2023-01-25 16:56:25 -07:00
zomars 755aae969c Removed pre-install script
devs should opt-in to submodules
2023-01-25 13:39:12 -07:00
Peer Richelsen 2d50d09ce6 added hacker news badge to readme 2023-01-25 13:52:08 +01:00
Peer Richelsen a22397dbb4 Update README.md 2023-01-25 13:44:28 +01:00
Mohammed Cherfaoui d1076982a7 fix: event location link modal (#6387) 2023-01-25 09:49:05 +00:00
Nafees Nazik d77e4f3c4a fix: decode characters if encoded (#6447) 2023-01-25 09:44:48 +00:00
Joe Cohen b1c0ea4c38 Fix duplicate event modal page slug (#6657) 2023-01-25 09:16:50 +00:00
Carina WollendorferandCarinaWolli 425e18f53c enable scroll (#6676)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-25 09:14:20 +00:00
Peer Richelsen 3611807838 Update README.md (#6682) 2023-01-25 09:13:33 +00:00
Udit Takkar 5b1cd9e5ce fix: send email if username is being passed (#6674)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-25 08:56:20 +00:00
Joe Au-Yeung 00bff598ef Add videoCallData to evt on new attendee (#6673)
* Add videoCallData to evt on new attendee

* Remove console.log
2023-01-25 08:52:58 +00:00
Nafees Nazik 3a8aa26c78 fix: duplicate iframes in cal video (#6662)
* chore: upgrade daily-co

* feat: return only needed data and preload i18n

* fix: useEffect
2023-01-25 08:51:09 +00:00
Carina WollendorferandCarinaWolli 3c761f4451 Fixes issues with workflows that have more than one action (#6670)
* fix untitled workflow name when no action exists

* fix wrong data loaded one first laod when step was added

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-25 08:42:57 +00:00
8640eb9f52 Ability to format user and team bio (#6572)
* use texteditor for bio

* remove block types from about editor

* add props to make editor height adjustable

* set isDirty to true when about input is edited

* add editor to getting-started

* fix editor height

* remove required error for onboarding

* add helper function to check if parsed bio has text

* add back commented code

* fix onboarding tests for optional about field

* rename function

* parse team bio for read only members

* code clean up

* fix failing e2e because of missing test id

* fix onboarding e2e test

* add missing parse of user bio

* Update apps/web/components/getting-started/steps-views/UserProfile.tsx

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

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

* use css inline style for height instead of tailwind class

* fix height of editor-input

* save bio as markdown in db

* fix empty line when bio is empty

* fix hydration failed error

* Update packages/ui/components/editor/Editor.tsx

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

* remove unused import

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-24 18:08:10 -07:00
Kiran K abb5fd36f3 Support OIDC (#6661)
* Support OIDC

* tweak to the Configure button

* fix the type import
2023-01-24 13:02:43 -07:00
Alex van Andel 865663646a v2.5.5
release.yml / release (push) Canceled after 0s
2023-01-24 16:54:35 +00:00
Alex van Andel b7c92063c2 Revert "v2.5.4"
This reverts commit 4f293edb6b.
2023-01-24 16:54:05 +00:00
Alex van Andel 4f293edb6b v2.5.4 2023-01-24 16:52:33 +00:00
Ben HybertandAlex van Andel 0c88cf75e0 corrected avatars inside users block (#6667)
* corrected avatars inside users block

* ensured the team avatar didnt shrink too

* Update packages/ui/components/avatar/Avatar.tsx

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-01-24 16:10:43 +00:00
Nafees NazikandUdit Takkar 0b93647209 fix: double border issue in Location Select (#6646)
* fix: border issue

* fix: remove border from dropdown

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 <udit.07814802719@cse.mait.ac.in>
2023-01-24 17:05:35 +01:00
Alex van Andel b931b00714 Add env variables to turbo.json relevant to the website build (#6654) 2023-01-24 15:47:46 +00:00
Nafees Nazik 796a5f8d51 feat: use cal video as the default location (#6643)
* feat: use cal video as default while creating new events

* feat: use cal video as default value

* chore: remove default value

* fix: default value logic

* chore: remove console.log
2023-01-24 15:28:40 +00:00
sean-brydon 905a110377 Invites (#6665) 2023-01-24 15:27:05 +00:00
Alex van Andel 6ccc19985f Add sizePropsBySize to avatarPrimitive root (#6666) 2023-01-24 14:01:27 +00:00
Udit Takkar 8f71474303 fix: add allowOverflow property (#6636)
* fix: add allowOverlfow property

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

* fix: date override input dialog

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

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-24 14:00:45 +00:00
zomars 5f351230ff Ignores auth submodule 2023-01-23 16:50:17 -07:00
Alex van Andel 3e005919c1 Only return enabled apps in getEnabledApps (#6653) 2023-01-23 16:27:20 -07:00
cfa8fd8b67 Reduce bundle size by importing single icons at a time (#6644)
* Removed barrel import for icons to reduce bundle size.

* Fixed replacement mistakes

* Reverted unneccesary yarn.lock updates

* Added some missed Icon. import conversions

* Remove merge artifact import in @calcom/ui

* Don't import Icon in pages/[user]

* Update packages/ui/package.json

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Omar López <zomars@me.com>
2023-01-23 23:08:01 +00:00
Alex van Andel a888be6e04 Support both https/git by detection of origin root (#6651)
* Support both https/git by detection of origin root

* Remove unnecessary .

* Minor comment fixup
2023-01-23 23:05:53 +00:00
Joe Au-Yeung 6aa2cba3ce Msteams access token refresh (#6607)
* Add is valid call

* WIP

* Change logic for Microsoft refresh token

* remove async
2023-01-23 15:36:24 -07:00
sean-brydon 12ab615684 Fix weird avatar issue (#6645) 2023-01-23 15:48:40 +00:00
1d792a2c6c generic <UpgradeScreen> component (#6594)
* first attempt of <UpgradeScreen>

* changes to icons

* reverted changes back to initial state, needs fix: teams not showing

* WIP

* Fix weird reactnode error

* Fix loading text

* added upgradeTip to routing forms

* icon colors

* create and use hook to check if user has team plan

* use useTeamPlan for upgradeTeamsBadge

* replace huge svg with compressed jpeg

* responsive fixes

* Update packages/ui/components/badge/UpgradeTeamsBadge.tsx

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

* Give team plan features to E2E tests

* Allow option to make a user part of team int ests

* Remove flash of paywall for team user

* Add team user for typeform tests as well

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-01-23 10:58:41 +01:00
Hariom Balhara 9602ac29fb Fix Ga4 tracking (#6630) 2023-01-23 09:09:16 +00:00
Leo Giovanetti 5714471b9a Temporary fix (#6626)
release.yml / release (push) Canceled after 0s
2023-01-22 17:27:27 +00:00
Mohammed CherfaouiandAlex van Andel a99e93a4bd fix tooltip and modal stacking issues (#6491)
* fix tooltip and modal stacking issues

* use z-index in larger screens and less

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-01-22 20:31:25 +05:30
Udit Takkar 7f044ff92f fix: use items-center (#6618) 2023-01-22 13:36:07 +01:00
349 changed files with 4539 additions and 2630 deletions

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: buildjet-4vcpu-ubuntu-2204
steps: steps:
- name: Checkout repo - name: Checkout repo
+1
View File
@@ -86,3 +86,4 @@ apps/storybook/build-storybook.log
apps/api apps/api
apps/website apps/website
apps/console apps/console
apps/auth
+28 -8
View File
@@ -27,14 +27,13 @@
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a> <a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
<a href="https://status.cal.com"><img src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a> <a href="https://status.cal.com"><img src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a> <a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
<a href="https://news.ycombinator.com/item?id=26817795"><img src="https://img.shields.io/badge/Hacker%20News-311-%23FF6600" alt="Hacker News"></a> <a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
<a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a> <a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
<a href="https://github.com/calcom/cal.com/pulse"><img src="https://img.shields.io/github/commit-activity/m/calcom/cal.com" alt="Commits-per-month"></a> <a href="https://github.com/calcom/cal.com/pulse"><img src="https://img.shields.io/github/commit-activity/m/calcom/cal.com" alt="Commits-per-month"></a>
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-Free-brightgreen" alt="Pricing"></a> <a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-Free-brightgreen" alt="Pricing"></a>
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a> <a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
<img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" /> <img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" />
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a> <a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a> <a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a> <a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a> <a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
@@ -57,18 +56,39 @@ Calendly and other scheduling tools are awesome. It made our lives massively eas
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. 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.
## Product of the Month: April 2021 ## Recognition
#### Support us on [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso) #### Hacker News
<a href="https://news.ycombinator.com/item?id=34507672">
<img
style="width: 250px; height: 54px;" width="250" height="54"
alt="Featured on Hacker News"
src="https://hackernews-badge.vercel.app/api?id=34507672"
/>
</a>
<a href="https://news.ycombinator.com/item?id=26817795">
<img
style="width: 250px; height: 54px;" width="250" height="54"
alt="Featured on Hacker News"
src="https://hackernews-badge.vercel.app/api?id=26817795"
/>
</a>
#### [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso)
<a href="https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
### Built With ### Built With
- [Next.js](https://nextjs.org/) - [Next.js](https://nextjs.org/?ref=cal.com)
- [React](https://reactjs.org/) - [tRPC](https://trpc.io/?ref=cal.com)
- [Tailwind](https://tailwindcss.com/) - [React](https://reactjs.org/?ref=cal.com)
- [Prisma](https://prisma.io/) - [Tailwind](https://tailwindcss.com/?ref=cal.com)
- [Prisma](https://prisma.io/?ref=cal.com)
## Stay Up-to-Date ## Stay Up-to-Date
-80
View File
@@ -1,81 +1 @@
<!-- PROJECT LOGO -->
<div align="right">
<a href="https://github.com/calcom/cal.com">
<img src="https://user-images.githubusercontent.com/8019099/133430653-24422d2a-3c8d-4052-9ad6-0580597151ee.png" alt="Logo">
</a>
<a href="https://cal.com">Website</a>
·
<a href="https://github.com/calcom/cal.com/issues">Community Support</a>
</div>
# Cal.com Documentation
The official product, support and developer documentation, containing information and guides about using the product as well as support for self-hosted installations. This documentation site runs on [Nextra](https://nextra.vercel.app), so you may refer to their documentation should you need information on anything that isn't covered here.
## Prerequisites
- Git
- Node.js & npm
- Yarn
## Installation
Firstly, clone the repository using Git:
```console
git clone https://github.com/calcom/docs.git
```
Now, you can install the dependencies with yarn:
```console
yarn install
```
## Editing
To create, edit and delete documentation pages, you can simply create markdown (.mdx) files in the `pages/` folder. You can edit Markdown with any text editor, but VS Code and WebStorm have side-by-side previews so you can see your formatted content whilst writing markdown.
You will also need to add it as an entry to the `meta.json` file found in whichever directory that the .mdx file is in.
## Local Development
```console
yarn dev
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
## Build
```console
yarn build
```
This command generates static content into the `build` directory and can be served using any static content hosting service.
## How to easily contribute
## Existing Page
1. From the documentation's GitHub repository, head to the folder called 'pages' and open it.
2. From here you can view all current pages on the documentation site. Select the page you would like to contribute to.
3. You should now be able to view the page you have selected. Located at the top right of the page will be a pencil icon. Pressing this will bring you up an editor to edit and make changes. You can add formatting using the buttons at the top, which will automatically insert the relevant markdown content needed to style the text.
4. From here make the changes you wish to make.
5. At the bottom of the screen will be a 'Propose Changes' box, fill in all the relevant details such as title and description then press the green 'Propose Changes' button.
6. Your changes have been saved, to submit them for review, located on your screen, press the green 'Create Pull Request' button.
7. Fill in all the relevant details such as title and description and after finalize the submission.
You have now successfully edited and submitted changes to our documentation site.
## Creating a New Page
1. From the documentation's GitHub repository, head to the folder called 'pages' and open it.
2. From here you can view all current pages on the documentation site. At the top of your screen press the 'New file' button.
3. You should now be able to view the page you have created. Remember when renaming the document to put .mdx at the end of the file name.
4. From here make the changes you wish to make. Such as creating a title, sub-title and body text.
5. At the bottom of the screen will be a 'Propose Changes' box, fill in all the relevant details such as title and description then press the green 'Propose Changes' button.
6. Your changes have been saved, to submit them for review, located on your screen, press the greem 'Create Pull Request' button.
7. Fill in all the relevant details such as title and description and after finalize the submission.
You have now successfully created and submitted changes to our documentation site.
+1 -1
View File
@@ -49,7 +49,7 @@
"storybook-addon-next": "^1.6.9", "storybook-addon-next": "^1.6.9",
"storybook-react-i18next": "^1.1.2", "storybook-react-i18next": "^1.1.2",
"tailwindcss": "^3.2.1", "tailwindcss": "^3.2.1",
"typescript": "^4.7.4", "typescript": "^4.9.4",
"vite": "^2.9.15" "vite": "^2.9.15"
} }
} }
+1 -1
View File
@@ -24,6 +24,6 @@
"@types/node": "16.9.1", "@types/node": "16.9.1",
"@types/react": "^18.0.17", "@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"typescript": "^4.7.4" "typescript": "^4.9.4"
} }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui"; import { FiX } from "@calcom/ui/components/icon";
export default function AddToHomescreen() { export default function AddToHomescreen() {
const { t } = useLocale(); const { t } = useLocale();
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
type="button" type="button"
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white"> className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
<span className="sr-only">{t("dismiss")}</span> <span className="sr-only">{t("dismiss")}</span>
<Icon.FiX className="h-6 w-6 text-white" aria-hidden="true" /> <FiX className="h-6 w-6 text-white" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
+65 -7
View File
@@ -1,35 +1,93 @@
import { ReactNode } from "react"; import { useRouter } from "next/router";
import { ReactNode, useEffect, useRef, useState } from "react";
import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { Badge, ListItemText } from "@calcom/ui"; import { Badge, ListItemText } from "@calcom/ui";
import { FiAlertCircle } from "@calcom/ui/components/icon";
interface AppListCardProps { type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
type AppListCardProps = {
logo?: string; logo?: string;
title: string; title: string;
description: string; description: string;
actions?: ReactNode; actions?: ReactNode;
isDefault?: boolean; isDefault?: boolean;
} isTemplate?: boolean;
invalidCredential?: boolean;
children?: ReactNode;
} & ShouldHighlight;
const schema = z.object({ hl: z.string().optional() });
export default function AppListCard(props: AppListCardProps) { export default function AppListCard(props: AppListCardProps) {
const { t } = useLocale(); const { t } = useLocale();
const { logo, title, description, actions, isDefault } = props; const {
logo,
title,
description,
actions,
isDefault,
slug,
shouldHighlight,
isTemplate,
invalidCredential,
children,
} = props;
const {
data: { hl },
} = useTypedQuery(schema);
const router = useRouter();
const [highlight, setHighlight] = useState(shouldHighlight && hl === slug);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
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 });
}, 3000);
timeoutRef.current = timer;
}
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
}, []);
return ( return (
<div className="p-4"> <div className={`p-4 ${highlight ? "bg-yellow-100" : ""}`}>
<div className="flex items-center gap-x-3"> <div className="flex gap-x-3">
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null} {logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
<div className="flex grow flex-col gap-y-1 truncate"> <div className="flex grow flex-col gap-y-1 truncate">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3> <h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
{isDefault ? <Badge variant="green">{t("default")}</Badge> : null} <div className="flex items-center gap-x-2">
{isDefault && <Badge variant="green">{t("default")}</Badge>}
{isTemplate && <Badge variant="red">Template</Badge>}
</div>
</div> </div>
<ListItemText component="p">{description}</ListItemText> <ListItemText component="p">{description}</ListItemText>
{invalidCredential && (
<div className="flex gap-x-2 pt-2">
<FiAlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
{t("invalid_credential")}
</ListItemText>
</div>
)}
</div> </div>
{actions} {actions}
</div> </div>
{children && <div className="w-full">{children}</div>}
</div> </div>
); );
} }
+10 -10
View File
@@ -12,7 +12,6 @@ import {
DialogClose, DialogClose,
DialogContent, DialogContent,
HorizontalTabs, HorizontalTabs,
Icon,
InputLeading, InputLeading,
Label, Label,
showToast, showToast,
@@ -20,6 +19,7 @@ import {
TextArea, TextArea,
TextField, TextField,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
import ColorPicker from "@components/ui/colorpicker"; import ColorPicker from "@components/ui/colorpicker";
import Select from "@components/ui/form/Select"; import Select from "@components/ui/form/Select";
@@ -488,7 +488,7 @@ const tabs = [
{ {
name: "HTML", name: "HTML",
href: "embedTabName=embed-code", href: "embedTabName=embed-code",
icon: Icon.FiCode, icon: FiCode,
type: "code", type: "code",
Component: forwardRef< Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null, HTMLTextAreaElement | HTMLIFrameElement | null,
@@ -541,7 +541,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
{ {
name: "React", name: "React",
href: "embedTabName=embed-react", href: "embedTabName=embed-react",
icon: Icon.FiCode, icon: FiCode,
type: "code", type: "code",
Component: forwardRef< Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null, HTMLTextAreaElement | HTMLIFrameElement | null,
@@ -581,7 +581,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
{ {
name: "Preview", name: "Preview",
href: "embedTabName=embed-preview", href: "embedTabName=embed-preview",
icon: Icon.FiTrello, icon: FiTrello,
type: "iframe", type: "iframe",
Component: forwardRef< Component: forwardRef<
HTMLIFrameElement | HTMLTextAreaElement | null, HTMLIFrameElement | HTMLTextAreaElement | null,
@@ -617,7 +617,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => { const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
return ( return (
<components.Control {...props}> <components.Control {...props}>
<Icon.FiSun className="ml-2 h-4 w-4 text-gray-500" /> <FiSun className="ml-2 h-4 w-4 text-gray-500" />
{children} {children}
</components.Control> </components.Control>
); );
@@ -815,7 +815,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
onClick={() => { onClick={() => {
removeQueryParams(router, ["embedType", "embedTabName"]); removeQueryParams(router, ["embedType", "embedTabName"]);
}}> }}>
<Icon.FiArrowLeft className="mr-4 w-4" /> <FiArrowLeft className="mr-4 w-4" />
</button> </button>
{embed.title} {embed.title}
</h3> </h3>
@@ -835,7 +835,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
? "Floating Popup Customization" ? "Floating Popup Customization"
: "Element Click Customization"} : "Element Click Customization"}
</div> </div>
<Icon.FiChevronRight <FiChevronRight
className={`${ className={`${
isEmbedCustomizationOpen ? "rotate-90 transform" : "" isEmbedCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-gray-500`} } ml-auto h-5 w-5 text-gray-500`}
@@ -863,7 +863,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
}; };
}); });
}} }}
addOnLeading={<InputLeading>W</InputLeading>} addOnLeading={<>W</>}
/> />
<span className="p-2">×</span> <span className="p-2">×</span>
<TextField <TextField
@@ -883,7 +883,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
}; };
}); });
}} }}
addOnLeading={<InputLeading>H</InputLeading>} addOnLeading={<>H</>}
/> />
</div> </div>
</div> </div>
@@ -1002,7 +1002,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}> onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleTrigger className="flex w-full" type="button"> <CollapsibleTrigger className="flex w-full" type="button">
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div> <div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
<Icon.FiChevronRight <FiChevronRight
className={`${ className={`${
isBookingCustomizationOpen ? "rotate-90 transform" : "" isBookingCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-gray-500`} } ml-auto h-5 w-5 text-gray-500`}
+8 -7
View File
@@ -1,7 +1,8 @@
import React, { ComponentProps } from "react"; import React, { ComponentProps } from "react";
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { ErrorBoundary, Icon } from "@calcom/ui"; import { ErrorBoundary } from "@calcom/ui";
import { FiCreditCard, FiKey, FiLock, FiTerminal, FiUser, FiUsers } from "@calcom/ui/components/icon";
import NavTabs from "./NavTabs"; import NavTabs from "./NavTabs";
@@ -9,32 +10,32 @@ const tabs = [
{ {
name: "profile", name: "profile",
href: "/settings/profile", href: "/settings/profile",
icon: Icon.FiUser, icon: FiUser,
}, },
{ {
name: "teams", name: "teams",
href: "/settings/teams", href: "/settings/teams",
icon: Icon.FiUsers, icon: FiUsers,
}, },
{ {
name: "security", name: "security",
href: "/settings/security", href: "/settings/security",
icon: Icon.FiKey, icon: FiKey,
}, },
{ {
name: "developer", name: "developer",
href: "/settings/developer", href: "/settings/developer",
icon: Icon.FiTerminal, icon: FiTerminal,
}, },
{ {
name: "billing", name: "billing",
href: "/settings/billing", href: "/settings/billing",
icon: Icon.FiCreditCard, icon: FiCreditCard,
}, },
{ {
name: "admin", name: "admin",
href: "/settings/admin", href: "/settings/admin",
icon: Icon.FiLock, icon: FiLock,
adminRequired: true, adminRequired: true,
}, },
]; ];
@@ -4,7 +4,8 @@ import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App"; import type { App } from "@calcom/types/App";
import { Button, Icon, Select } from "@calcom/ui"; import { Button, Select } from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
@@ -31,7 +32,7 @@ const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App
/> />
) : ( ) : (
<Button className="w-full" color="minimal" href="/apps/categories/calendar"> <Button className="w-full" color="minimal" href="/apps/categories/calendar">
<Icon.FiPlus className="text-color mr-3 ml-1 h-4 w-4" /> <FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
<p>{t("install_new_calendar_app")}</p> <p>{t("install_new_calendar_app")}</p>
</Button> </Button>
); );
@@ -61,7 +62,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
<Select <Select
name="additionalCalendar" name="additionalCalendar"
placeholder={ placeholder={
<Button StartIcon={Icon.FiPlus} color="secondary"> <Button StartIcon={FiPlus} color="secondary">
{t("add")} {t("add")}
</Button> </Button>
} }
+21 -15
View File
@@ -12,7 +12,17 @@ import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/consta
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { App as AppType } from "@calcom/types/App"; import { App as AppType } from "@calcom/types/App";
import { Button, Icon, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui"; import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
import {
FiBookOpen,
FiCheck,
FiExternalLink,
FiFile,
FiFlag,
FiMail,
FiPlus,
FiShield,
} from "@calcom/ui/components/icon";
const Component = ({ const Component = ({
name, name,
@@ -118,7 +128,7 @@ const Component = ({
isGlobal || isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? ( (existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3"> <div className="flex space-x-3">
<Button StartIcon={Icon.FiCheck} color="secondary" disabled> <Button StartIcon={FiCheck} color="secondary" disabled>
{existingCredentials.length > 0 {existingCredentials.length > 0
? t("active_install", { count: existingCredentials.length }) ? t("active_install", { count: existingCredentials.length })
: t("default")} : t("default")}
@@ -139,7 +149,7 @@ const Component = ({
} }
return ( return (
<Button <Button
StartIcon={Icon.FiPlus} StartIcon={FiPlus}
{...props} {...props}
// @TODO: Overriding color and size prevent us from // @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now. // having to duplicate InstallAppButton for now.
@@ -200,7 +210,7 @@ const Component = ({
</span> </span>
)} )}
<div className="prose prose-sm mt-8">{body}</div> <div className="prose-sm prose mt-8">{body}</div>
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4> <h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
<span> <span>
{price === 0 ? ( {price === 0 ? (
@@ -226,7 +236,7 @@ const Component = ({
rel="noreferrer" rel="noreferrer"
className="text-sm font-normal text-black no-underline hover:underline" className="text-sm font-normal text-black no-underline hover:underline"
href={docs}> href={docs}>
<Icon.FiBookOpen className="mr-1 -mt-1 inline h-4 w-4 text-gray-500" /> <FiBookOpen className="mr-1 -mt-1 inline h-4 w-4 text-gray-500" />
{t("documentation")} {t("documentation")}
</a> </a>
</li> </li>
@@ -238,7 +248,7 @@ const Component = ({
rel="noreferrer" rel="noreferrer"
className="font-normal text-black no-underline hover:underline" className="font-normal text-black no-underline hover:underline"
href={website}> href={website}>
<Icon.FiExternalLink className="mr-1 -mt-px inline h-4 w-4 text-gray-500" /> <FiExternalLink className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
{website.replace("https://", "")} {website.replace("https://", "")}
</a> </a>
</li> </li>
@@ -250,7 +260,7 @@ const Component = ({
rel="noreferrer" rel="noreferrer"
className="font-normal text-black no-underline hover:underline" className="font-normal text-black no-underline hover:underline"
href={"mailto:" + email}> href={"mailto:" + email}>
<Icon.FiMail className="mr-1 -mt-px inline h-4 w-4 text-gray-500" /> <FiMail className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
{email} {email}
</a> </a>
@@ -263,7 +273,7 @@ const Component = ({
rel="noreferrer" rel="noreferrer"
className="font-normal text-black no-underline hover:underline" className="font-normal text-black no-underline hover:underline"
href={tos}> href={tos}>
<Icon.FiFile className="mr-1 -mt-px inline h-4 w-4 text-gray-500" /> <FiFile className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
{t("terms_of_service")} {t("terms_of_service")}
</a> </a>
</li> </li>
@@ -275,7 +285,7 @@ const Component = ({
rel="noreferrer" rel="noreferrer"
className="font-normal text-black no-underline hover:underline" className="font-normal text-black no-underline hover:underline"
href={privacy}> href={privacy}>
<Icon.FiShield className="mr-1 -mt-px inline h-4 w-4 text-gray-500" /> <FiShield className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
{t("privacy_policy")} {t("privacy_policy")}
</a> </a>
</li> </li>
@@ -286,7 +296,7 @@ const Component = ({
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })} {t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span> </span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}> <a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
<Icon.FiFlag className="inline h-3 w-3" /> {t("report_app")} <FiFlag className="inline h-3 w-3" /> {t("report_app")}
</a> </a>
</div> </div>
</div> </div>
@@ -321,15 +331,11 @@ export default function App(props: {
const { t } = useLocale(); const { t } = useLocale();
return ( return (
<Shell large isPublic heading={t("app_store")} backPath="/apps" withoutSeo> <Shell smallHeading isPublic heading={t("app_store")} backPath="/apps" withoutSeo>
<HeadSeo <HeadSeo
title={props.name} title={props.name}
description={props.description} description={props.description}
app={{ slug: props.logo, name: props.name, description: props.description }} app={{ slug: props.logo, name: props.name, description: props.description }}
nextSeoProps={{
nofollow: true,
noindex: true,
}}
/> />
{props.licenseRequired ? ( {props.licenseRequired ? (
<LicenseRequired> <LicenseRequired>
@@ -11,18 +11,18 @@ import {
Alert, Alert,
Button, Button,
EmptyScreen, EmptyScreen,
Icon,
List, List,
showToast, showToast,
AppSkeletonLoader as SkeletonLoader, AppSkeletonLoader as SkeletonLoader,
Switch, Switch,
ShellSubHeading, ShellSubHeading,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiArrowLeft, FiCalendar, FiPlus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
import AppListCard from "@components/AppListCard";
import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector"; import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector";
import IntegrationListItem from "@components/apps/IntegrationListItem";
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections"; import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
type Props = { type Props = {
@@ -100,7 +100,7 @@ function CalendarSwitch(props: {
/> />
{!!props.destination && ( {!!props.destination && (
<span className="ml-4 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800"> <span className="ml-4 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800">
<Icon.FiArrowLeft className="h-4 w-4" /> <FiArrowLeft className="h-4 w-4" />
{t("adding_events_to")} {t("adding_events_to")}
</span> </span>
)} )}
@@ -118,13 +118,13 @@ function CalendarList(props: Props) {
success={({ data }) => ( success={({ data }) => (
<List> <List>
{data.items.map((item) => ( {data.items.map((item) => (
<IntegrationListItem <AppListCard
name={item.name} title={item.name}
slug={item.slug} key={item.name}
key={item.title}
title={item.title}
logo={item.logo} logo={item.logo}
description={item.description} description={item.description}
shouldHighlight
slug={item.slug}
actions={ actions={
<InstallAppButton <InstallAppButton
type={item.type} type={item.type}
@@ -161,16 +161,16 @@ function ConnectedCalendarsList(props: Props) {
} }
return ( return (
<List className="flex flex-col gap-6" noBorderTreatment> <List>
{data.connectedCalendars.map((item) => ( {data.connectedCalendars.map((item) => (
<Fragment key={item.credentialId}> <Fragment key={item.credentialId}>
{item.calendars ? ( {item.calendars ? (
<IntegrationListItem <AppListCard
shouldHighlight
slug={item.integration.slug} slug={item.integration.slug}
title={item.integration.title} title={item.integration.name}
logo={item.integration.logo} logo={item.integration.logo}
description={item.primary?.email ?? item.integration.description} description={item.primary?.email ?? item.integration.description}
separate={true}
actions={ actions={
<div className="flex w-32 justify-end"> <div className="flex w-32 justify-end">
<DisconnectIntegration <DisconnectIntegration
@@ -200,7 +200,7 @@ function ConnectedCalendarsList(props: Props) {
</> </>
)} )}
</div> </div>
</IntegrationListItem> </AppListCard>
) : ( ) : (
<Alert <Alert
severity="warning" severity="warning"
@@ -282,8 +282,8 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
<div className="flex justify-between rounded-md border border-gray-200 bg-gray-50 p-4"> <div className="flex justify-between rounded-md border border-gray-200 bg-gray-50 p-4">
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center"> <div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
<div className="relative rounded-md border border-gray-200 bg-white p-1.5"> <div className="relative rounded-md border border-gray-200 bg-white p-1.5">
<Icon.FiCalendar className="h-8 w-8" strokeWidth="1" /> <FiCalendar className="h-8 w-8" strokeWidth="1" />
<Icon.FiPlus <FiPlus
className="absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2 text-black" className="absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2 text-black"
strokeWidth="4" strokeWidth="4"
/> />
@@ -322,7 +322,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
</> </>
) : ( ) : (
<EmptyScreen <EmptyScreen
Icon={Icon.FiCalendar} Icon={FiCalendar}
headline={t("no_category_apps", { headline={t("no_category_apps", {
category: t("calendar").toLowerCase(), category: t("calendar").toLowerCase(),
})} })}
@@ -1,80 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { ReactNode, useEffect, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge, Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui";
import classNames from "@lib/classNames";
function IntegrationListItem(props: {
imageSrc?: string;
slug: string;
name?: string;
title?: string;
description: string;
actions?: ReactNode;
children?: ReactNode;
logo: string;
destination?: boolean;
separate?: boolean;
invalidCredential?: boolean;
isTemplate?: boolean;
}): JSX.Element {
const { t } = useLocale();
const router = useRouter();
const { hl } = router.query;
const [highlight, setHighlight] = useState(hl === props.slug);
const title = props.name || props.title;
// The highlight is to show a newly installed app, coming from the app's
// redirection after installation, so we proceed to show the corresponding
// message
if (highlight) {
showToast(t("app_successfully_installed"), "success");
}
useEffect(() => {
const timer = setTimeout(() => setHighlight(false), 3000);
return () => {
clearTimeout(timer);
};
}, []);
return (
<ListItem
expanded={!!props.children}
className={classNames(
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
"my-0 flex-col border transition-colors duration-500",
highlight ? "bg-yellow-100" : ""
)}>
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
{props.logo && <img className="h-11 w-11" src={props.logo} alt={title} />}
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3" className="flex ">
<Link href={"/apps/" + props.slug}>{props.name || title}</Link>
{props.isTemplate && (
<Badge variant="red" className="ml-4">
Template
</Badge>
)}
</ListItemTitle>
<ListItemText component="p">{props.description}</ListItemText>
{/* Alert error that key stopped working. */}
{props.invalidCredential && (
<div className="flex items-center space-x-2 rtl:space-x-reverse">
<Icon.FiAlertCircle className="w-8 text-red-500 sm:w-4" />
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
{t("invalid_credential")}
</ListItemText>
</div>
)}
</div>
<div>{props.actions}</div>
</div>
{props.children && <div className="w-full">{props.children}</div>}
</ListItem>
);
}
export default IntegrationListItem;
@@ -4,7 +4,8 @@ import React, { ComponentProps } from "react";
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen, Icon } from "@calcom/ui"; import { EmptyScreen } from "@calcom/ui";
import { FiAlertCircle } from "@calcom/ui/components/icon";
type AppsLayoutProps = { type AppsLayoutProps = {
children: React.ReactNode; children: React.ReactNode;
@@ -25,7 +26,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
<main className="w-full"> <main className="w-full">
{emptyStore ? ( {emptyStore ? (
<EmptyScreen <EmptyScreen
Icon={Icon.FiAlertCircle} Icon={FiAlertCircle}
headline={t("no_apps")} headline={t("no_apps")}
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""} description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""} buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
@@ -4,39 +4,39 @@ import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavi
import { InstalledAppVariants } from "@calcom/app-store/utils"; import { InstalledAppVariants } from "@calcom/app-store/utils";
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui";
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui"; import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
import { FiBarChart, FiCalendar, FiCreditCard, FiGrid, FiShare2, FiVideo } from "@calcom/ui/components/icon";
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [ const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{ {
name: "calendar", name: "calendar",
href: "/apps/installed/calendar", href: "/apps/installed/calendar",
icon: Icon.FiCalendar, icon: FiCalendar,
}, },
{ {
name: "conferencing", name: "conferencing",
href: "/apps/installed/conferencing", href: "/apps/installed/conferencing",
icon: Icon.FiVideo, icon: FiVideo,
}, },
{ {
name: "payment", name: "payment",
href: "/apps/installed/payment", href: "/apps/installed/payment",
icon: Icon.FiCreditCard, icon: FiCreditCard,
}, },
{ {
name: "automation", name: "automation",
href: "/apps/installed/automation", href: "/apps/installed/automation",
icon: Icon.FiShare2, icon: FiShare2,
}, },
{ {
name: "analytics", name: "analytics",
href: "/apps/installed/analytics", href: "/apps/installed/analytics",
icon: Icon.FiBarChart, icon: FiBarChart,
}, },
{ {
name: "other", name: "other",
href: "/apps/installed/other", href: "/apps/installed/other",
icon: Icon.FiGrid, icon: FiGrid,
}, },
]; ];
@@ -57,9 +57,7 @@ export default function InstalledAppsLayout({
return ( return (
<Shell {...rest}> <Shell {...rest}>
<AppCategoryNavigation <AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
baseURL="/apps/installed"
containerClassname="w-full xl:mx-5 xl:w-4/5 xl:max-w-2xl xl:pr-5">
{children} {children}
</AppCategoryNavigation> </AppCategoryNavigation>
</Shell> </Shell>
@@ -10,6 +10,7 @@ import { UserPermissionRole } from ".prisma/client";
export default function AdminLayout({ export default function AdminLayout({
children, children,
...rest ...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) { }: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
const session = useSession(); const session = useSession();
@@ -22,10 +23,11 @@ export default function AdminLayout({
} }
}, [session, router]); }, [session, router]);
const isAppsPage = router.asPath.startsWith("/settings/admin/apps");
return ( return (
<SettingsLayout {...rest}> <SettingsLayout {...rest}>
<div className="mx-auto flex max-w-4xl flex-row divide-y divide-gray-200"> <div className="mx-auto flex max-w-4xl flex-row divide-y divide-gray-200">
<div className="flex flex-1 [&>*]:flex-1"> <div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
<ErrorBoundary>{children}</ErrorBoundary> <ErrorBoundary>{children}</ErrorBoundary>
</div> </div>
</div> </div>
@@ -1,7 +1,8 @@
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations"; import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon, Tooltip } from "@calcom/ui"; import { Tooltip } from "@calcom/ui";
import { FiLink } from "@calcom/ui/components/icon";
import { Props } from "./pages/AvailabilityPage"; import { Props } from "./pages/AvailabilityPage";
@@ -19,7 +20,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
return ( return (
<div key={location.type} className="flex flex-row items-center text-sm font-medium"> <div key={location.type} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? ( {eventLocationType.iconUrl === "/link.svg" ? (
<Icon.FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " /> <FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
) : ( ) : (
<img <img
src={eventLocationType.iconUrl} src={eventLocationType.iconUrl}
@@ -4,7 +4,8 @@ import { FC, ReactNode, useEffect } from "react";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon, Badge } from "@calcom/ui"; import { Badge } from "@calcom/ui";
import { FiCheckSquare, FiClock, FiInfo } from "@calcom/ui/components/icon";
import useRouterQuery from "@lib/hooks/useRouterQuery"; import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -93,7 +94,7 @@ const BookingDescription: FC<Props> = (props) => {
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600" isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
)}> )}>
<div> <div>
<Icon.FiInfo <FiInfo
className={classNames( className={classNames(
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]", "ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
isBookingPage && "dark:text-darkgray-600 -mt-1 text-gray-500" isBookingPage && "dark:text-darkgray-600 -mt-1 text-gray-500"
@@ -112,7 +113,7 @@ const BookingDescription: FC<Props> = (props) => {
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600" isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
)}> )}>
<div> <div>
<Icon.FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " /> <FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
</div> </div>
{requiresConfirmationText} {requiresConfirmationText}
</div> </div>
@@ -123,9 +124,10 @@ const BookingDescription: FC<Props> = (props) => {
<div <div
className={classNames( className={classNames(
"flex flex-nowrap text-sm font-medium", "flex flex-nowrap text-sm font-medium",
isBookingPage && "dark:text-darkgray-600 text-gray-600" isBookingPage && "dark:text-darkgray-600 text-gray-600",
!eventType.metadata?.multipleDuration && "items-center"
)}> )}>
<Icon.FiClock <FiClock
className={classNames( className={classNames(
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]", "min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
isBookingPage && "mt-[2px]" isBookingPage && "mt-[2px]"
@@ -18,7 +18,6 @@ import {
DialogContent, DialogContent,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
Icon,
MeetingTimeInTimezones, MeetingTimeInTimezones,
showToast, showToast,
TextArea, TextArea,
@@ -26,6 +25,7 @@ import {
ActionType, ActionType,
TableActions, TableActions,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
import useMeQuery from "@lib/hooks/useMeQuery"; import useMeQuery from "@lib/hooks/useMeQuery";
@@ -100,7 +100,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => { onClick: () => {
setRejectionDialogIsOpen(true); setRejectionDialogIsOpen(true);
}, },
icon: Icon.FiSlash, icon: FiSlash,
disabled: mutation.isLoading, disabled: mutation.isLoading,
}, },
{ {
@@ -109,7 +109,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => { onClick: () => {
bookingConfirm(true); bookingConfirm(true);
}, },
icon: Icon.FiCheck, icon: FiCheck,
disabled: mutation.isLoading, disabled: mutation.isLoading,
color: "primary", color: "primary",
}, },
@@ -135,7 +135,7 @@ function BookingListItem(booking: BookingItemProps) {
href: `/booking/${booking.uid}?cancel=true${ href: `/booking/${booking.uid}?cancel=true${
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : "" isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}`, }`,
icon: Icon.FiX, icon: FiX,
}, },
{ {
id: "edit_booking", id: "edit_booking",
@@ -143,13 +143,13 @@ function BookingListItem(booking: BookingItemProps) {
actions: [ actions: [
{ {
id: "reschedule", id: "reschedule",
icon: Icon.FiClock, icon: FiClock,
label: t("reschedule_booking"), label: t("reschedule_booking"),
href: `/reschedule/${booking.uid}`, href: `/reschedule/${booking.uid}`,
}, },
{ {
id: "reschedule_request", id: "reschedule_request",
icon: Icon.FiSend, icon: FiSend,
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ", iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
label: t("send_reschedule_request"), label: t("send_reschedule_request"),
onClick: () => { onClick: () => {
@@ -162,7 +162,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => { onClick: () => {
setIsOpenLocationDialog(true); setIsOpenLocationDialog(true);
}, },
icon: Icon.FiMapPin, icon: FiMapPin,
}, },
], ],
}, },
@@ -179,7 +179,7 @@ function BookingListItem(booking: BookingItemProps) {
const RequestSentMessage = () => { const RequestSentMessage = () => {
return ( return (
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent"> <div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
<Icon.FiSend className="-mt-[1px] w-4 rotate-45" /> <FiSend className="-mt-[1px] w-4 rotate-45" />
<p className="ml-2 ">{t("reschedule_request_sent")}</p> <p className="ml-2 ">{t("reschedule_request_sent")}</p>
</div> </div>
); );
@@ -455,7 +455,7 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
); );
})}> })}>
<div className="text-gray-600 dark:text-white"> <div className="text-gray-600 dark:text-white">
<Icon.FiRefreshCcw <FiRefreshCcw
strokeWidth="3" strokeWidth="3"
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400" className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
/> />
@@ -5,7 +5,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RecurringEvent } from "@calcom/types/Calendar"; import type { RecurringEvent } from "@calcom/types/Calendar";
import { Button, Icon, TextArea } from "@calcom/ui"; import { Button, TextArea } from "@calcom/ui";
import { FiX } from "@calcom/ui/components/icon";
type Props = { type Props = {
booking: { booking: {
@@ -28,7 +29,7 @@ export default function CancelBooking(props: Props) {
const [cancellationReason, setCancellationReason] = useState<string>(""); const [cancellationReason, setCancellationReason] = useState<string>("");
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
const { booking, profile, team, allRemainingBookings } = props; const { booking, allRemainingBookings } = props;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled")); const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
@@ -39,7 +40,7 @@ export default function CancelBooking(props: Props) {
{error && ( {error && (
<div className="mt-8"> <div className="mt-8">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100"> <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<Icon.FiX className="h-6 w-6 text-red-600" /> <FiX className="h-6 w-6 text-red-600" />
</div> </div>
<div className="mt-3 text-center sm:mt-5"> <div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title"> <h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
@@ -27,8 +27,10 @@ import notEmpty from "@calcom/lib/notEmpty";
import { getRecurringFreq } from "@calcom/lib/recurringStrings"; import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat"; import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Icon, HeadSeo } from "@calcom/ui"; import { HeadSeo } from "@calcom/ui";
import { FiChevronDown, FiChevronUp, FiCreditCard, FiGlobe, FiRefreshCcw } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock"; import { timeZone as localStorageTimeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery"; import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -97,7 +99,10 @@ const SlotPicker = ({
weekStart = 0, weekStart = 0,
ethSignature, ethSignature,
}: { }: {
eventType: Pick<EventType, "id" | "schedulingType" | "slug">; eventType: Pick<
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
"id" | "schedulingType" | "slug" | "length" | "metadata"
>;
timeFormat: TimeFormat; timeFormat: TimeFormat;
onTimeFormatChange: (is24Hour: boolean) => void; onTimeFormatChange: (is24Hour: boolean) => void;
timeZone?: string; timeZone?: string;
@@ -109,11 +114,15 @@ const SlotPicker = ({
}) => { }) => {
const [selectedDate, setSelectedDate] = useState<Dayjs>(); const [selectedDate, setSelectedDate] = useState<Dayjs>();
const [browsingDate, setBrowsingDate] = useState<Dayjs>(); const [browsingDate, setBrowsingDate] = useState<Dayjs>();
const { duration } = useRouterQuery("duration"); let { duration = eventType.length.toString() } = useRouterQuery("duration");
const { date, setQuery: setDate } = useRouterQuery("date"); const { date, setQuery: setDate } = useRouterQuery("date");
const { month, setQuery: setMonth } = useRouterQuery("month"); const { month, setQuery: setMonth } = useRouterQuery("month");
const router = useRouter(); const router = useRouter();
if (!eventType.metadata?.multipleDuration) {
duration = eventType.length.toString();
}
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>(); const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
useEffect(() => { useEffect(() => {
@@ -220,12 +229,12 @@ function TimezoneDropdown({
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}> <Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600"> <Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
<p className="flex items-center text-sm font-medium"> <p className="flex items-center text-sm font-medium">
<Icon.FiGlobe className="min-h-4 min-w-4 ml-[2px] -mt-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]" /> <FiGlobe className="min-h-4 min-w-4 ml-[2px] -mt-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]" />
{timeZone} {timeZone}
{isTimeOptionsOpen ? ( {isTimeOptionsOpen ? (
<Icon.FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" /> <FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
) : ( ) : (
<Icon.FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" /> <FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
)} )}
</p> </p>
</Popover.Trigger> </Popover.Trigger>
@@ -367,7 +376,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}> <BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
{!rescheduleUid && eventType.recurringEvent && ( {!rescheduleUid && eventType.recurringEvent && (
<div className="flex items-start text-sm font-medium"> <div className="flex items-start text-sm font-medium">
<Icon.FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " /> <FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<div> <div>
<p className="mb-1 -ml-2 inline px-2 py-1"> <p className="mb-1 -ml-2 inline px-2 py-1">
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })} {getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
@@ -392,7 +401,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
)} )}
{stripeAppData.price > 0 && ( {stripeAppData.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium"> <p className="-ml-2 px-2 text-sm font-medium">
<Icon.FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" /> <FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en"> <IntlProvider locale="en">
<FormattedNumber <FormattedNumber
value={stripeAppData.price / 100.0} value={stripeAppData.price / 100.0}
+155 -85
View File
@@ -6,9 +6,8 @@ import { useSession } from "next-auth/react";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useMemo, useReducer, useState } from "react"; import { useEffect, useMemo, useReducer, useState } from "react";
import { Controller, useForm, useWatch } from "react-hook-form"; import { useFieldArray, useForm, useWatch, Controller } from "react-hook-form";
import { FormattedNumber, IntlProvider } from "react-intl"; import { FormattedNumber, IntlProvider } from "react-intl";
import { ReactMultiEmail } from "react-multi-email";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { z } from "zod"; import { z } from "zod";
@@ -38,9 +37,29 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import slugify from "@calcom/lib/slugify";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { AddressInput, Button, EmailInput, Form, Icon, PhoneInput, Tooltip } from "@calcom/ui"; import {
import { Group, RadioField } from "@calcom/ui"; AddressInput,
Button,
EmailField,
EmailInput,
Form,
Group,
PhoneInput,
RadioField,
Tooltip,
} from "@calcom/ui";
import {
FiUserPlus,
FiCalendar,
FiX,
FiInfo,
FiCreditCard,
FiRefreshCw,
FiUser,
FiAlertTriangle,
} from "@calcom/ui/components/icon";
import { asStringOrNull } from "@lib/asStringOrNull"; import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock"; import { timeZone } from "@lib/clock";
@@ -49,7 +68,6 @@ import useRouterQuery from "@lib/hooks/useRouterQuery";
import createBooking from "@lib/mutations/bookings/create-booking"; import createBooking from "@lib/mutations/bookings/create-booking";
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking"; import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
import { parseDate, parseRecurringDates } from "@lib/parseDate"; import { parseDate, parseRecurringDates } from "@lib/parseDate";
import slugify from "@lib/slugify";
import Gates, { Gate, GateState } from "@components/Gates"; import Gates, { Gate, GateState } from "@components/Gates";
import BookingDescription from "@components/booking/BookingDescription"; import BookingDescription from "@components/booking/BookingDescription";
@@ -65,7 +83,7 @@ type BookingFormValues = {
email: string; email: string;
notes?: string; notes?: string;
locationType?: EventLocationType["type"]; locationType?: EventLocationType["type"];
guests?: string[]; guests?: { email: string }[];
address?: string; address?: string;
attendeeAddress?: string; attendeeAddress?: string;
phone?: string; phone?: string;
@@ -107,7 +125,12 @@ const BookingPage = ({
const stripeAppData = getStripeAppData(eventType); const stripeAppData = getStripeAppData(eventType);
// Define duration now that we support multiple duration eventTypes // Define duration now that we support multiple duration eventTypes
let duration = eventType.length; let duration = eventType.length;
if (queryDuration && !isNaN(Number(queryDuration))) { if (
queryDuration &&
!isNaN(Number(queryDuration)) &&
eventType.metadata?.multipleDuration &&
eventType.metadata?.multipleDuration.includes(Number(queryDuration))
) {
duration = Number(queryDuration); duration = Number(queryDuration);
} }
@@ -184,7 +207,9 @@ const BookingPage = ({
const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id; const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id;
const guestListEmails = !isDynamicGroupBooking const guestListEmails = !isDynamicGroupBooking
? booking?.attendees.slice(1).map((attendee) => attendee.email) ? booking?.attendees.slice(1).map((attendee) => {
return { email: attendee.email };
})
: []; : [];
// There should only exists one default userData variable for primaryAttendee. // There should only exists one default userData variable for primaryAttendee.
@@ -203,7 +228,9 @@ const BookingPage = ({
name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "", name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "",
email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "", email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "",
notes: (router.query.notes as string) || "", notes: (router.query.notes as string) || "",
guests: ensureArray(router.query.guest) as string[], guests: ensureArray(router.query.guest).map((guest) => {
return { email: guest as string };
}),
customInputs: eventType.customInputs.reduce( customInputs: eventType.customInputs.reduce(
(customInputs, input) => ({ (customInputs, input) => ({
...customInputs, ...customInputs,
@@ -245,6 +272,7 @@ const BookingPage = ({
.object({ .object({
name: z.string().min(1), name: z.string().min(1),
email: z.string().trim().email(), email: z.string().trim().email(),
guests: z.array(z.object({ email: z.string().email() })).optional(),
phone: z phone: z
.string() .string()
.refine((val) => isValidPhoneNumber(val)) .refine((val) => isValidPhoneNumber(val))
@@ -263,6 +291,10 @@ const BookingPage = ({
defaultValues: defaultValues(), defaultValues: defaultValues(),
resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema
}); });
const guestsField = useFieldArray({
name: "guests",
control: bookingForm.control,
});
const selectedLocationType = useWatch({ const selectedLocationType = useWatch({
control: bookingForm.control, control: bookingForm.control,
@@ -301,6 +333,7 @@ const BookingPage = ({
} }
const bookEvent = (booking: BookingFormValues) => { const bookEvent = (booking: BookingFormValues) => {
bookingForm.clearErrors();
const bookingCustomInputs = Object.keys(booking.customInputs || {}).map((inputId) => ({ const bookingCustomInputs = Object.keys(booking.customInputs || {}).map((inputId) => ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "", label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
value: booking.customInputs && booking.customInputs[inputId] ? booking.customInputs[inputId] : "", value: booking.customInputs && booking.customInputs[inputId] ? booking.customInputs[inputId] : "",
@@ -368,6 +401,29 @@ const BookingPage = ({
} }
} }
// Validate that guests are unique
let alreadyInvited = false;
booking.guests?.forEach((guest, index) => {
if (guest.email === booking.email) {
bookingForm.setError(`guests.${index}`, { type: "validate", message: t("already_invited") });
alreadyInvited = true;
}
if (booking.guests) {
let guestCount = 0;
for (const checkGuest of booking.guests) {
if (checkGuest.email === guest.email) guestCount++;
if (guestCount > 1) {
bookingForm.setError(`guests.${index}`, { type: "validate", message: t("already_invited") });
alreadyInvited = true;
break;
}
}
}
});
if (alreadyInvited) return;
if (recurringDates.length) { if (recurringDates.length) {
// Identify set of bookings to one intance of recurring event to support batch changes // Identify set of bookings to one intance of recurring event to support batch changes
const recurringEventId = uuidv4(); const recurringEventId = uuidv4();
@@ -398,6 +454,7 @@ const BookingPage = ({
? booking.phone ? booking.phone
: booking.smsReminderNumber || undefined, : booking.smsReminderNumber || undefined,
ethSignature: gateState.rainbowToken, ethSignature: gateState.rainbowToken,
guests: booking.guests?.map((guest) => guest.email),
})); }));
recurringMutation.mutate(recurringBookings); recurringMutation.mutate(recurringBookings);
} else { } else {
@@ -426,6 +483,7 @@ const BookingPage = ({
? booking.phone ? booking.phone
: booking.smsReminderNumber || undefined, : booking.smsReminderNumber || undefined,
ethSignature: gateState.rainbowToken, ethSignature: gateState.rainbowToken,
guests: booking.guests?.map((guest) => guest.email),
}); });
} }
}; };
@@ -479,7 +537,7 @@ const BookingPage = ({
})}{" "} })}{" "}
| {APP_NAME} | {APP_NAME}
</title> </title>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favico.ico" />
</Head> </Head>
<BookingPageTagManager eventType={eventType} /> <BookingPageTagManager eventType={eventType} />
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} /> <CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
@@ -501,7 +559,7 @@ const BookingPage = ({
<BookingDescription isBookingPage profile={profile} eventType={eventType}> <BookingDescription isBookingPage profile={profile} eventType={eventType}>
{stripeAppData.price > 0 && ( {stripeAppData.price > 0 && (
<p className="text-bookinglight -ml-2 px-2 text-sm "> <p className="text-bookinglight -ml-2 px-2 text-sm ">
<Icon.FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" /> <FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en"> <IntlProvider locale="en">
<FormattedNumber <FormattedNumber
value={stripeAppData.price / 100.0} value={stripeAppData.price / 100.0}
@@ -513,7 +571,7 @@ const BookingPage = ({
)} )}
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && ( {!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
<div className="items-start text-sm font-medium text-gray-600 dark:text-white"> <div className="items-start text-sm font-medium text-gray-600 dark:text-white">
<Icon.FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" /> <FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<p className="-ml-2 inline-block items-center px-2"> <p className="-ml-2 inline-block items-center px-2">
{getEveryFreqFor({ {getEveryFreqFor({
t, t,
@@ -524,7 +582,7 @@ const BookingPage = ({
</div> </div>
)} )}
<div className="text-bookinghighlight flex items-start text-sm"> <div className="text-bookinghighlight flex items-start text-sm">
<Icon.FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" /> <FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<div className="text-sm font-medium"> <div className="text-sm font-medium">
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`} {(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
{!rescheduleUid && {!rescheduleUid &&
@@ -552,14 +610,14 @@ const BookingPage = ({
{t("former_time")} {t("former_time")}
</p> </p>
<p className="line-through "> <p className="line-through ">
<Icon.FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" /> <FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)} {typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
</p> </p>
</div> </div>
)} )}
{!!eventType.seatsPerTimeSlot && ( {!!eventType.seatsPerTimeSlot && (
<div className="text-bookinghighlight flex items-start text-sm"> <div className="text-bookinghighlight flex items-start text-sm">
<Icon.FiUser <FiUser
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${ className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5 booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600" ? "text-rose-600"
@@ -623,7 +681,7 @@ const BookingPage = ({
/> />
{bookingForm.formState.errors.email && ( {bookingForm.formState.errors.email && (
<div className="mt-2 flex items-center text-sm text-red-700 "> <div className="mt-2 flex items-center text-sm text-red-700 ">
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" /> <FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p>{t("email_validation_error")}</p> <p>{t("email_validation_error")}</p>
</div> </div>
)} )}
@@ -716,7 +774,7 @@ const BookingPage = ({
</div> </div>
{bookingForm.formState.errors.phone && ( {bookingForm.formState.errors.phone && (
<div className="mt-2 flex items-center text-sm text-red-700 "> <div className="mt-2 flex items-center text-sm text-red-700 ">
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" /> <FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p>{t("invalid_number")}</p> <p>{t("invalid_number")}</p>
</div> </div>
)} )}
@@ -834,7 +892,7 @@ const BookingPage = ({
/> />
{bookingForm.formState.errors?.customInputs?.[input.id] && ( {bookingForm.formState.errors?.customInputs?.[input.id] && (
<div className="mt-2 flex items-center text-sm text-red-700 "> <div className="mt-2 flex items-center text-sm text-red-700 ">
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" /> <FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p>{t("invalid_number")}</p> <p>{t("invalid_number")}</p>
</div> </div>
)} )}
@@ -842,60 +900,7 @@ const BookingPage = ({
)} )}
</div> </div>
))} ))}
{!eventType.disableGuests && guestToggle && (
<div className="mb-4">
<div>
<label
htmlFor="guests"
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
{t("guests")}
</label>
{!disableInput && (
<Controller
control={bookingForm.control}
name="guests"
render={({ field: { onChange, value } }) => (
<ReactMultiEmail
className="relative"
placeholder={<span className="dark:text-darkgray-600">guest@example.com</span>}
emails={value}
onChange={onChange}
getLabel={(
email: string,
index: number,
removeEmail: (index: number) => void
) => {
return (
<div data-tag key={index} className="cursor-pointer">
{email}
{!disableInput && (
<span data-tag-handle onClick={() => removeEmail(index)}>
×
</span>
)}
</div>
);
}}
/>
)}
/>
)}
{/* Custom code when guest emails should not be editable */}
{disableInput && guestListEmails && guestListEmails.length > 0 && (
<div data-tag className="react-multi-email">
{/* // @TODO: user owners are appearing as guest here when should be only user input */}
{guestListEmails.map((email, index) => {
return (
<div key={index} className="cursor-pointer">
<span data-tag>{email}</span>
</div>
);
})}
</div>
)}
</div>
</div>
)}
{isSmsReminderNumberNeeded && selectedLocationType !== LocationType.Phone && ( {isSmsReminderNumberNeeded && selectedLocationType !== LocationType.Phone && (
<div className="mb-4"> <div className="mb-4">
<label <label
@@ -914,7 +919,7 @@ const BookingPage = ({
</div> </div>
{bookingForm.formState.errors.smsReminderNumber && ( {bookingForm.formState.errors.smsReminderNumber && (
<div className="mt-2 flex items-center text-sm text-red-700 "> <div className="mt-2 flex items-center text-sm text-red-700 ">
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" /> <FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p>{t("invalid_number")}</p> <p>{t("invalid_number")}</p>
</div> </div>
)} )}
@@ -948,19 +953,84 @@ const BookingPage = ({
/> />
)} )}
</div> </div>
{!eventType.disableGuests && guestsField.fields.length ? (
<div className="mb-4">
<div>
<label
htmlFor="guests"
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
{t("guests")}
</label>
<ul>
{guestsField.fields.map((field, index) => (
<li key={field.id}>
<EmailField
{...bookingForm.register(`guests.${index}.email` as const)}
className={classNames(
inputClassName,
bookingForm.formState.errors.guests?.[index] &&
"!focus:ring-red-700 !border-red-700",
"border-r-0"
)}
addOnClassname={classNames(
"border-gray-300 border block border-l-0 disabled:bg-gray-200 disabled:hover:cursor-not-allowed bg-transparent disabled:text-gray-500 dark:border-darkgray-300 ",
bookingForm.formState.errors.guests?.[index] &&
"!focus:ring-red-700 !border-red-700"
)}
placeholder="guest@example.com"
label={<></>}
required
addOnSuffix={
<Tooltip content="Remove guest">
<button
className="m-1 disabled:hover:cursor-not-allowed"
type="button"
onClick={() => guestsField.remove(index)}>
<FiX className="text-gray-600" />
</button>
</Tooltip>
}
/>
{bookingForm.formState.errors.guests?.[index] && (
<div className="mt-2 flex items-center text-sm text-red-700 ">
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p className="text-red-700">
{bookingForm.formState.errors.guests?.[index]?.message}
</p>
</div>
)}
</li>
))}
</ul>
<Button
type="button"
color="minimal"
StartIcon={FiUserPlus}
className="my-2.5"
// className="mb-1 block text-sm font-medium text-gray-700 dark:text-white"
onClick={() => guestsField.append({ email: "" })}>
{t("add_another")}
</Button>
</div>
</div>
) : (
<></>
)}
{!eventType.disableGuests && !guestsField.fields.length && (
<Button
color="minimal"
variant="button"
StartIcon={FiUserPlus}
onClick={() => {
guestsField.append({ email: "" });
}}
className="mr-auto">
{t("additional_guests")}
</Button>
)}
<div className="flex justify-end space-x-2 rtl:space-x-reverse"> <div className="flex justify-end space-x-2 rtl:space-x-reverse">
{!eventType.disableGuests && !guestToggle && (
<Button
type="button"
color="minimal"
variant="icon"
tooltip={t("additional_guests")}
StartIcon={Icon.FiUserPlus}
onClick={() => setGuestToggle(!guestToggle)}
className="mr-auto"
/>
)}
<Button color="minimal" type="button" onClick={() => router.back()}> <Button color="minimal" type="button" onClick={() => router.back()}>
{t("cancel")} {t("cancel")}
</Button> </Button>
@@ -993,7 +1063,7 @@ function ErrorMessage({ error }: { error: unknown }) {
<div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4"> <div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Icon.FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" /> <FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div> </div>
<div className="ltr:ml-3 rtl:mr-3"> <div className="ltr:ml-3 rtl:mr-3">
<p className="text-sm text-yellow-700"> <p className="text-sm text-yellow-700">
@@ -1,6 +1,8 @@
import { ErrorMessage } from "@hookform/error-message"; import { ErrorMessage } from "@hookform/error-message";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js"; import { isValidPhoneNumber } from "libphonenumber-js";
import { Trans } from "next-i18next";
import Link from "next/link";
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm, useWatch } from "react-hook-form"; import { Controller, useForm, useWatch } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -15,7 +17,8 @@ import {
} from "@calcom/app-store/locations"; } from "@calcom/app-store/locations";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogContent, DialogFooter, Form, Icon, PhoneInput } from "@calcom/ui"; import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
import { FiMapPin } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
@@ -204,10 +207,10 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
return ( return (
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}> <Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
<DialogContent disableOverflow> <DialogContent>
<div className="flex flex-row space-x-3"> <div className="flex flex-row space-x-3">
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10"> <div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
<Icon.FiMapPin className="text-primary-600 h-6 w-6" /> <FiMapPin className="text-primary-600 h-6 w-6" />
</div> </div>
<div className="w-full"> <div className="w-full">
<div className="mt-3 text-center sm:mt-0 sm:text-left"> <div className="mt-3 text-center sm:mt-0 sm:text-left">
@@ -215,7 +218,15 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
{t("edit_location")} {t("edit_location")}
</h3> </h3>
{!booking && ( {!booking && (
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p> <p className="text-sm text-gray-400">
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
Can&apos;t find the right video app? Visit our
<Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video">
App Store
</Link>
.
</Trans>
</p>
)} )}
</div> </div>
<div className="mt-3 text-center sm:mt-0 sm:text-left" /> <div className="mt-3 text-center sm:mt-0 sm:text-left" />
@@ -288,30 +299,31 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
name="locationType" name="locationType"
control={locationFormMethods.control} control={locationFormMethods.control}
render={() => ( render={() => (
<LocationSelect <div className="py-4">
maxMenuHeight={300} <LocationSelect
name="location" maxMenuHeight={300}
defaultValue={selection} name="location"
options={locationOptions} defaultValue={selection}
isSearchable options={locationOptions}
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 text-sm" isSearchable
onChange={(val) => { onChange={(val) => {
if (val) { if (val) {
locationFormMethods.setValue("locationType", val.value); locationFormMethods.setValue("locationType", val.value);
locationFormMethods.unregister([ locationFormMethods.unregister([
"locationLink", "locationLink",
"locationAddress", "locationAddress",
"locationPhoneNumber", "locationPhoneNumber",
]); ]);
locationFormMethods.clearErrors([ locationFormMethods.clearErrors([
"locationLink", "locationLink",
"locationPhoneNumber", "locationPhoneNumber",
"locationAddress", "locationAddress",
]); ]);
setSelectedLocation?.(val); setSelectedLocation?.(val);
} }
}} }}
/> />
</div>
)} )}
/> />
); );
@@ -325,7 +337,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
setShowLocationModal(false); setShowLocationModal(false);
setSelectedLocation?.(undefined); setSelectedLocation?.(undefined);
setEditingLocationType?.(""); setEditingLocationType?.("");
locationFormMethods.unregister("locationType"); locationFormMethods.unregister(["locationType", "locationLink"]);
}} }}
type="button" type="button"
color="secondary"> color="secondary">
@@ -9,10 +9,10 @@ import {
DialogContent, DialogContent,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
Icon,
showToast, showToast,
TextArea, TextArea,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiClock } from "@calcom/ui/components/icon";
interface IRescheduleDialog { interface IRescheduleDialog {
isOpenDialog: boolean; isOpenDialog: boolean;
@@ -43,7 +43,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
<DialogContent> <DialogContent>
<div className="flex flex-row space-x-3"> <div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]"> <div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<Icon.FiClock className="m-auto h-6 w-6" /> <FiClock className="m-auto h-6 w-6" />
</div> </div>
<div className="pt-1"> <div className="pt-1">
<DialogHeader title={t("send_reschedule_request")} /> <DialogHeader title={t("send_reschedule_request")} />
@@ -8,7 +8,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday"; import { weekdayNames } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Badge, Button, Icon, Select, SettingsToggle, SkeletonText } from "@calcom/ui"; import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader"; import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
@@ -164,13 +165,13 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
<hr /> <hr />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between"> <div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start"> <span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
<Icon.FiGlobe className="ltr:mr-2 rtl:ml-2" /> <FiGlobe className="ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />} {schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span> </span>
<Button <Button
href={`/availability/${schedule?.schedule.id}`} href={`/availability/${schedule?.schedule.id}`}
color="minimal" color="minimal"
EndIcon={Icon.FiExternalLink} EndIcon={FiExternalLink}
target="_blank" target="_blank"
className="justify-center border sm:border-0" className="justify-center border sm:border-0"
rel="noopener noreferrer"> rel="noopener noreferrer">
@@ -5,7 +5,8 @@ import { FC } from "react";
import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form"; import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, Label, Select, TextField } from "@calcom/ui"; import { Button, Label, Select, TextField } from "@calcom/ui";
import { FiPlus, FiX } from "@calcom/ui/components/icon";
interface OptionTypeBase { interface OptionTypeBase {
label: string; label: string;
@@ -77,7 +78,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
defaultValue={selectedInputOption} defaultValue={selectedInputOption}
options={inputOptions} options={inputOptions}
isSearchable={false} isSearchable={false}
className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm" className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm"
onChange={(option) => option && field.onChange(option.value)} onChange={(option) => option && field.onChange(option.value)}
value={selectedInputOption} value={selectedInputOption}
onBlur={field.onBlur} onBlur={field.onBlur}
@@ -185,7 +186,7 @@ function RadioInputHandler({
<Button <Button
variant="icon" variant="icon"
color="minimal" color="minimal"
StartIcon={Icon.FiX} StartIcon={FiX}
onClick={() => { onClick={() => {
remove(index); remove(index);
}} }}
@@ -196,7 +197,7 @@ function RadioInputHandler({
))} ))}
<Button <Button
color="minimal" color="minimal"
StartIcon={Icon.FiPlus} StartIcon={FiPlus}
className="!text-sm !font-medium" className="!text-sm !font-medium"
onClick={() => { onClick={() => {
append({ label: "", type: "text" }); append({ label: "", type: "text" });
@@ -5,6 +5,8 @@ import { Controller, useFormContext } from "react-hook-form";
import short from "short-uuid"; import short from "short-uuid";
import { v5 as uuidv5 } from "uuid"; import { v5 as uuidv5 } from "uuid";
import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } from "@calcom/core/event";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector"; import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import CustomInputItem from "@calcom/features/eventtypes/components/CustomInputItem"; import CustomInputItem from "@calcom/features/eventtypes/components/CustomInputItem";
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants"; import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
@@ -18,13 +20,13 @@ import {
DialogClose, DialogClose,
DialogContent, DialogContent,
DialogFooter, DialogFooter,
Icon,
Label, Label,
SettingsToggle, SettingsToggle,
showToast, showToast,
TextField, TextField,
Tooltip, Tooltip,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiEdit, FiCopy, FiPlus } from "@calcom/ui/components/icon";
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm"; import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
@@ -57,6 +59,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink); const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl); const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link); const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const eventNameObject: EventNameObjectType = {
attendeeName: t("scheduler"),
eventType: eventType.title,
eventName: eventType.eventName,
host: eventType.users[0]?.name || "Nameless",
t,
};
const [previewText, setPreviewText] = useState(getEventName(eventNameObject));
const [customInputs, setCustomInputs] = useState<CustomInputParsed[]>( const [customInputs, setCustomInputs] = useState<CustomInputParsed[]>(
eventType.customInputs.sort((a, b) => a.id - b.id) || [] eventType.customInputs.sort((a, b) => a.id - b.id) || []
); );
@@ -72,6 +82,15 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
setCustomInputs([...customInputs]); setCustomInputs([...customInputs]);
}; };
const changePreviewText = (eventNameObject: EventNameObjectType, previewEventName: string) => {
setPreviewText(
previewEventName
.replace("{Event type title}", eventNameObject.eventType)
.replace("{Scheduler}", eventNameObject.attendeeName)
.replace("{Organiser}", eventNameObject.host)
);
};
useEffect(() => { useEffect(() => {
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id)); !hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
}, [eventType.users, hashedUrl, team?.id]); }, [eventType.users, hashedUrl, team?.id]);
@@ -120,15 +139,22 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
)} )}
<div className="w-full"> <div className="w-full">
<TextField <TextField
label={t("event_name")} label={t("event_name_in_calendar")}
type="text" type="text"
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })} placeholder={t("meeting_with_user")}
defaultValue={eventType.eventName || ""} defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")} {...formMethods.register("eventName", {
onChange: (e) => {
if (!e.target.value || !formMethods.getValues("eventName")) {
return setPreviewText(getEventName(eventNameObject));
}
changePreviewText(eventNameObject, e.target.value);
},
})}
addOnSuffix={ addOnSuffix={
<Button <Button
type="button" type="button"
StartIcon={Icon.FiEdit} StartIcon={FiEdit}
variant="icon" variant="icon"
color="minimal" color="minimal"
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black" className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
@@ -168,7 +194,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
</ul> </ul>
{customInputs.length > 0 && ( {customInputs.length > 0 && (
<Button <Button
StartIcon={Icon.FiPlus} StartIcon={FiPlus}
color="minimal" color="minimal"
type="button" type="button"
onClick={() => { onClick={() => {
@@ -314,7 +340,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
}} }}
className="hover:stroke-3 hover:bg-transparent hover:text-black" className="hover:stroke-3 hover:bg-transparent hover:text-black"
type="button"> type="button">
<Icon.FiCopy /> <FiCopy />
</Button> </Button>
</Tooltip> </Tooltip>
} }
@@ -384,23 +410,63 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
description={t("custom_event_name_description")} description={t("custom_event_name_description")}
type="creation"> type="creation">
<TextField <TextField
label={t("event_name")} label={t("event_name_in_calendar")}
type="text" type="text"
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })} placeholder={t("meeting_with_user")}
defaultValue={eventType.eventName || ""} defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")} {...formMethods.register("eventName", {
onChange: (e) => {
if (!e.target.value || !formMethods.getValues("eventName")) {
return setPreviewText(getEventName(eventNameObject));
}
changePreviewText(eventNameObject, e.target.value);
},
})}
className="mb-0"
/> />
<div className="mt-1 text-gray-500"> <div className="text-sm">
<p>{`{HOST} = ${t("your_name")}`}</p> <div className="mb-6 rounded-md bg-gray-100 p-2">
<p>{`{ATTENDEE} = ${t("attendee_name")}`}</p> <h1 className="mb-2 ml-1 font-medium text-gray-900">{t("available_variables")}</h1>
<p>{`{HOST/ATTENDEE} = ${t("dynamically_display_attendee_or_organizer")}`}</p> <div className="mb-2.5 flex font-normal">
<p>{`{LOCATION} = ${t("event_location")}`}</p> <p className="ml-1 mr-5 w-28 text-gray-400">{`{Event type title}`}</p>
<p className="text-gray-900">{t("event_name_info")}</p>
</div>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Organiser}`}</p>
<p className="text-gray-900">{t("your_full_name")}</p>
</div>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Scheduler}`}</p>
<p className="text-gray-900">{t("scheduler_full_name")}</p>
</div>
<div className="mb-1 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Location}`}</p>
<p className="text-gray-900">{t("location_info")}</p>
</div>
</div>
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
<div
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center"
style={{
backgroundImage: "url(/calendar-preview.svg)",
}}>
<div className="m-auto flex items-center justify-center self-stretch">
<div className="mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black bg-gray-100 text-[12px] leading-3">
<p className="overflow-hidden text-ellipsis p-1.5 font-medium text-gray-900">
{previewText}
</p>
<p className="ml-1.5 text-[10px] font-normal text-gray-600">8 - 10 AM</p>
</div>
</div>
</div>
</div> </div>
<DialogFooter> <DialogFooter>
<DialogClose onClick={() => formMethods.setValue("eventName", eventType.eventName ?? "")}>
{t("cancel")}
</DialogClose>
<Button color="primary" onClick={() => setShowEventNameTip(false)}> <Button color="primary" onClick={() => setShowEventNameTip(false)}>
{t("create")} {t("create")}
</Button> </Button>
<DialogClose onClick={() => formMethods.setValue("eventName", eventType.eventName ?? "")} />
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -413,7 +479,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Dialog open={selectedCustomInputModalOpen} onOpenChange={setSelectedCustomInputModalOpen}> <Dialog open={selectedCustomInputModalOpen} onOpenChange={setSelectedCustomInputModalOpen}>
<DialogContent <DialogContent
type="creation" type="creation"
Icon={Icon.FiPlus} Icon={FiPlus}
title={t("add_new_custom_input_field")} title={t("add_new_custom_input_field")}
description={t("this_input_will_shown_booking_this_event")}> description={t("this_input_will_shown_booking_this_event")}>
<CustomInputTypeForm <CustomInputTypeForm
@@ -7,7 +7,8 @@ import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
import { EventTypeAppsList } from "@calcom/app-store/utils"; import { EventTypeAppsList } from "@calcom/app-store/utils";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, EmptyScreen, Icon } from "@calcom/ui"; import { Button, EmptyScreen } from "@calcom/ui";
import { FiGrid } from "@calcom/ui/components/icon";
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] & export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
EventTypeAppCardComponentProps["eventType"]; EventTypeAppCardComponentProps["eventType"];
@@ -60,7 +61,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
<div className="before:border-0"> <div className="before:border-0">
{!isLoading && !installedApps?.length ? ( {!isLoading && !installedApps?.length ? (
<EmptyScreen <EmptyScreen
Icon={Icon.FiGrid} Icon={FiGrid}
headline={t("empty_installed_apps_headline")} headline={t("empty_installed_apps_headline")}
description={t("empty_installed_apps_description")} description={t("empty_installed_apps_description")}
buttonRaw={ buttonRaw={
@@ -10,7 +10,8 @@ import findDurationType from "@calcom/lib/findDurationType";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { PeriodType } from "@calcom/prisma/client"; import { PeriodType } from "@calcom/prisma/client";
import type { BookingLimit } from "@calcom/types/Calendar"; import type { BookingLimit } from "@calcom/types/Calendar";
import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui"; import { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
const MinimumBookingNoticeInput = React.forwardRef< const MinimumBookingNoticeInput = React.forwardRef<
HTMLInputElement, HTMLInputElement,
@@ -419,19 +420,21 @@ const BookingLimits = () => {
defaultValue={BOOKING_LIMIT_OPTIONS.find((option) => option.value === key)} defaultValue={BOOKING_LIMIT_OPTIONS.find((option) => option.value === key)}
onChange={(val) => { onChange={(val) => {
const current = currentBookingLimits; const current = currentBookingLimits;
const currentValue = watchBookingLimits[bookingLimitKey];
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here) // Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
delete current[bookingLimitKey]; delete current[bookingLimitKey];
const newData = { const newData = {
...current, ...current,
// Set limit to new selected value (in the example above this means we set the limit to per_week here). // Set limit to new selected value (in the example above this means we set the limit to per_week here).
[val?.value as BookingLimitsKey]: watchBookingLimits[bookingLimitKey], [val?.value as BookingLimitsKey]: currentValue,
}; };
onChange(newData); onChange(newData);
}} }}
/> />
<Button <Button
variant="icon" variant="icon"
StartIcon={Icon.FiTrash} StartIcon={FiTrash}
color="destructive" color="destructive"
onClick={() => { onClick={() => {
const current = currentBookingLimits; const current = currentBookingLimits;
@@ -445,7 +448,7 @@ const BookingLimits = () => {
{currentBookingLimits && Object.keys(currentBookingLimits).length <= 3 && ( {currentBookingLimits && Object.keys(currentBookingLimits).length <= 3 && (
<Button <Button
color="minimal" color="minimal"
StartIcon={Icon.FiPlus} StartIcon={FiPlus}
onClick={() => { onClick={() => {
if (!currentBookingLimits || !watchBookingLimits) return; if (!currentBookingLimits || !watchBookingLimits) return;
const currentKeys = Object.keys(watchBookingLimits); const currentKeys = Object.keys(watchBookingLimits);
@@ -458,7 +461,7 @@ const BookingLimits = () => {
setValue("bookingLimits", { setValue("bookingLimits", {
...watchBookingLimits, ...watchBookingLimits,
[rest[0].value]: undefined, [rest[0].value]: 1,
}); });
}}> }}>
{t("add_limit")} {t("add_limit")}
@@ -12,9 +12,9 @@ import { z } from "zod";
import { EventLocationType, getEventLocationType, MeetLocationType } from "@calcom/app-store/locations"; import { EventLocationType, getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui"; import { slugify } from "@calcom/lib/slugify";
import { Button, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
import { slugify } from "@lib/slugify"; import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import LocationSelect, { import LocationSelect, {
@@ -138,15 +138,18 @@ export const EventSetupTab = (
return true; return true;
}); });
const defaultValue = locationOptions.find((item) => item.label === "video")?.options;
return ( return (
<div className="w-full"> <div className="w-full">
{validLocations.length === 0 && ( {validLocations.length === 0 && (
<div className="flex"> <div className="flex">
<LocationSelect <LocationSelect
defaultValue={defaultValue}
placeholder={t("select")} placeholder={t("select")}
options={locationOptions} options={locationOptions}
isSearchable={false} isSearchable={false}
className="block w-full min-w-0 flex-1 rounded-sm text-sm" className="block w-full min-w-0 flex-1 rounded-sm text-sm"
menuPlacement="auto"
onChange={(e: SingleValueLocationOption) => { onChange={(e: SingleValueLocationOption) => {
if (e?.value) { if (e?.value) {
const newLocationType = e.value; const newLocationType = e.value;
@@ -198,10 +201,10 @@ export const EventSetupTab = (
}} }}
aria-label={t("edit")} aria-label={t("edit")}
className="mr-1 p-1 text-gray-500 hover:text-gray-900"> className="mr-1 p-1 text-gray-500 hover:text-gray-900">
<Icon.FiEdit2 className="h-4 w-4" /> <FiEdit2 className="h-4 w-4" />
</button> </button>
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}> <button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
<Icon.FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " /> <FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
</button> </button>
</div> </div>
</div> </div>
@@ -210,7 +213,7 @@ export const EventSetupTab = (
})} })}
{validLocations.some((location) => location.type === MeetLocationType) && ( {validLocations.some((location) => location.type === MeetLocationType) && (
<div className="flex text-sm text-gray-600"> <div className="flex text-sm text-gray-600">
<Icon.FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" /> <FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
<Trans i18nKey="event_type_requres_google_cal"> <Trans i18nKey="event_type_requres_google_cal">
<p> <p>
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work. The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
@@ -227,7 +230,7 @@ export const EventSetupTab = (
)} )}
{validLocations.length > 0 && validLocations.length !== locationOptions.length && ( {validLocations.length > 0 && validLocations.length !== locationOptions.length && (
<li> <li>
<Button StartIcon={Icon.FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}> <Button StartIcon={FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
{t("add_location")} {t("add_location")}
</Button> </Button>
</li> </li>
@@ -1,4 +1,4 @@
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { EventTypeSetupProps } from "pages/event-types/[type]";
import { useState } from "react"; import { useState } from "react";
import { TbWebhook } from "react-icons/tb"; import { TbWebhook } from "react-icons/tb";
@@ -9,7 +9,8 @@ import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Webhook } from "@calcom/prisma/client"; import { Webhook } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogContent, EmptyScreen, Icon, showToast } from "@calcom/ui"; import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon";
export const EventTeamWebhooksTab = ({ export const EventTeamWebhooksTab = ({
eventType, eventType,
@@ -84,7 +85,7 @@ export const EventTeamWebhooksTab = ({
<Button <Button
color="secondary" color="secondary"
data-testid="new_webhook" data-testid="new_webhook"
StartIcon={Icon.FiPlus} StartIcon={FiPlus}
onClick={() => setCreateModalOpen(true)}> onClick={() => setCreateModalOpen(true)}>
{t("new_webhook")} {t("new_webhook")}
</Button> </Button>
@@ -130,7 +131,10 @@ export const EventTeamWebhooksTab = ({
{/* New webhook dialog */} {/* New webhook dialog */}
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}> <Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
<DialogContent title={t("create_webhook")} description={t("create_webhook_team_event_type")}> <DialogContent
enableOverflow
title={t("create_webhook")}
description={t("create_webhook_team_event_type")}>
<WebhookForm <WebhookForm
onSubmit={onCreateWebhook} onSubmit={onCreateWebhook}
onCancel={() => setCreateModalOpen(false)} onCancel={() => setCreateModalOpen(false)}
@@ -23,7 +23,6 @@ import {
DropdownItem, DropdownItem,
DropdownMenuTrigger, DropdownMenuTrigger,
HorizontalTabs, HorizontalTabs,
Icon,
Label, Label,
showToast, showToast,
Skeleton, Skeleton,
@@ -32,6 +31,21 @@ import {
VerticalDivider, VerticalDivider,
VerticalTabs, VerticalTabs,
} from "@calcom/ui"; } from "@calcom/ui";
import {
FiLink,
FiCalendar,
FiClock,
FiSliders,
FiRepeat,
FiGrid,
FiZap,
FiUsers,
FiExternalLink,
FiCode,
FiTrash,
FiMoreHorizontal,
FiLoader,
} from "@calcom/ui/components/icon";
import { EmbedButton, EmbedDialog } from "@components/Embed"; import { EmbedButton, EmbedDialog } from "@components/Embed";
@@ -63,44 +77,44 @@ function getNavigation(props: {
{ {
name: "event_setup_tab_title", name: "event_setup_tab_title",
href: `/event-types/${eventType.id}?tabName=setup`, href: `/event-types/${eventType.id}?tabName=setup`,
icon: Icon.FiLink, icon: FiLink,
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
}, },
{ {
name: "availability", name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`, href: `/event-types/${eventType.id}?tabName=availability`,
icon: Icon.FiCalendar, icon: FiCalendar,
info: `default_schedule_name`, // TODO: Get this from props info: `default_schedule_name`, // TODO: Get this from props
}, },
{ {
name: "event_limit_tab_title", name: "event_limit_tab_title",
href: `/event-types/${eventType.id}?tabName=limits`, href: `/event-types/${eventType.id}?tabName=limits`,
icon: Icon.FiClock, icon: FiClock,
info: `event_limit_tab_description`, info: `event_limit_tab_description`,
}, },
{ {
name: "event_advanced_tab_title", name: "event_advanced_tab_title",
href: `/event-types/${eventType.id}?tabName=advanced`, href: `/event-types/${eventType.id}?tabName=advanced`,
icon: Icon.FiSliders, icon: FiSliders,
info: `event_advanced_tab_description`, info: `event_advanced_tab_description`,
}, },
{ {
name: "recurring", name: "recurring",
href: `/event-types/${eventType.id}?tabName=recurring`, href: `/event-types/${eventType.id}?tabName=recurring`,
icon: Icon.FiRepeat, icon: FiRepeat,
info: `recurring_event_tab_description`, info: `recurring_event_tab_description`,
}, },
{ {
name: "apps", name: "apps",
href: `/event-types/${eventType.id}?tabName=apps`, href: `/event-types/${eventType.id}?tabName=apps`,
icon: Icon.FiGrid, icon: FiGrid,
//TODO: Handle proper translation with count handling //TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`, info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
}, },
{ {
name: "workflows", name: "workflows",
href: `/event-types/${eventType.id}?tabName=workflows`, href: `/event-types/${eventType.id}?tabName=workflows`,
icon: Icon.FiZap, icon: FiZap,
info: `${enabledWorkflowsNumber} ${t("active")}`, info: `${enabledWorkflowsNumber} ${t("active")}`,
}, },
]; ];
@@ -157,7 +171,7 @@ function EventTypeSingleLayout({
navigation.splice(2, 0, { navigation.splice(2, 0, {
name: "assignment", name: "assignment",
href: `/event-types/${eventType.id}?tabName=team`, href: `/event-types/${eventType.id}?tabName=team`,
icon: Icon.FiUsers, icon: FiUsers,
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin", info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
}); });
navigation.push({ navigation.push({
@@ -181,7 +195,6 @@ function EventTypeSingleLayout({
backPath="/event-types" backPath="/event-types"
title={eventType.title + " | " + t("event_type")} title={eventType.title + " | " + t("event_type")}
heading={eventType.title} heading={eventType.title}
subtitle={eventType.description || ""}
CTA={ CTA={
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<div className="hidden items-center rounded-md px-2 sm:flex sm:hover:bg-gray-100"> <div className="hidden items-center rounded-md px-2 sm:flex sm:hover:bg-gray-100">
@@ -211,14 +224,14 @@ function EventTypeSingleLayout({
variant="icon" variant="icon"
href={permalink} href={permalink}
rel="noreferrer" rel="noreferrer"
StartIcon={Icon.FiExternalLink} StartIcon={FiExternalLink}
/> />
</Tooltip> </Tooltip>
<Button <Button
color="secondary" color="secondary"
variant="icon" variant="icon"
StartIcon={Icon.FiLink} StartIcon={FiLink}
tooltip={t("copy_link")} tooltip={t("copy_link")}
onClick={() => { onClick={() => {
navigator.clipboard.writeText(permalink); navigator.clipboard.writeText(permalink);
@@ -227,7 +240,7 @@ function EventTypeSingleLayout({
/> />
<EmbedButton <EmbedButton
embedUrl={encodeURIComponent(embedLink)} embedUrl={encodeURIComponent(embedLink)}
StartIcon={Icon.FiCode} StartIcon={FiCode}
color="secondary" color="secondary"
variant="icon" variant="icon"
tooltip={t("embed")} tooltip={t("embed")}
@@ -235,7 +248,7 @@ function EventTypeSingleLayout({
<Button <Button
color="secondary" color="secondary"
variant="icon" variant="icon"
StartIcon={Icon.FiTrash} StartIcon={FiTrash}
tooltip={t("delete")} tooltip={t("delete")}
disabled={!hasPermsToDelete} disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)} onClick={() => setDeleteDialogOpen(true)}
@@ -246,19 +259,14 @@ function EventTypeSingleLayout({
<Dropdown> <Dropdown>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
className="lg:hidden"
StartIcon={Icon.FiMoreHorizontal}
variant="icon"
color="secondary"
/>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem className="focus:ring-gray-100"> <DropdownMenuItem className="focus:ring-gray-100">
<DropdownItem <DropdownItem
target="_blank" target="_blank"
type="button" type="button"
StartIcon={Icon.FiExternalLink} StartIcon={FiExternalLink}
href={permalink} href={permalink}
rel="noreferrer"> rel="noreferrer">
{t("preview")} {t("preview")}
@@ -267,7 +275,7 @@ function EventTypeSingleLayout({
<DropdownMenuItem className="focus:ring-gray-100"> <DropdownMenuItem className="focus:ring-gray-100">
<DropdownItem <DropdownItem
type="button" type="button"
StartIcon={Icon.FiLink} StartIcon={FiLink}
onClick={() => { onClick={() => {
navigator.clipboard.writeText(permalink); navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success"); showToast("Link copied!", "success");
@@ -278,7 +286,7 @@ function EventTypeSingleLayout({
<DropdownMenuItem className="focus:ring-gray-100"> <DropdownMenuItem className="focus:ring-gray-100">
<DropdownItem <DropdownItem
type="button" type="button"
StartIcon={Icon.FiTrash} StartIcon={FiTrash}
disabled={!hasPermsToDelete} disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}> onClick={() => setDeleteDialogOpen(true)}>
{t("delete")} {t("delete")}
@@ -313,8 +321,8 @@ function EventTypeSingleLayout({
</Button> </Button>
</div> </div>
}> }>
<Suspense fallback={<Icon.FiLoader />}> <Suspense fallback={<FiLoader />}>
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8"> <div className="flex flex-col xl:flex-row xl:space-x-6">
<div className="hidden xl:block"> <div className="hidden xl:block">
<VerticalTabs <VerticalTabs
className="primary-navigation" className="primary-navigation"
@@ -1,4 +1,5 @@
import { Icon, SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui"; import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
import { FiClock, FiUser } from "@calcom/ui/components/icon";
function SkeletonLoader() { function SkeletonLoader() {
return ( return (
@@ -31,11 +32,11 @@ function SkeletonItem() {
<div className=""> <div className="">
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse "> <ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
<li className="flex items-center whitespace-nowrap"> <li className="flex items-center whitespace-nowrap">
<Icon.FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" /> <FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<SkeletonText className="h-4 w-12" /> <SkeletonText className="h-4 w-12" />
</li> </li>
<li className="flex items-center whitespace-nowrap"> <li className="flex items-center whitespace-nowrap">
<Icon.FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" /> <FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<SkeletonText className="h-4 w-12" /> <SkeletonText className="h-4 w-12" />
</li> </li>
</ul> </ul>
@@ -1,5 +1,3 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { CalendarSwitch } from "./CalendarSwitch"; import { CalendarSwitch } from "./CalendarSwitch";
interface IConnectedCalendarItem { interface IConnectedCalendarItem {
@@ -21,7 +19,7 @@ interface IConnectedCalendarItem {
const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => { const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
const { name, logo, externalId, calendars, integrationType } = prop; const { name, logo, externalId, calendars, integrationType } = prop;
const { t } = useLocale();
return ( return (
<> <>
<div className="flex flex-row items-center p-4"> <div className="flex flex-row items-center p-4">
@@ -1,15 +1,19 @@
import { ArrowRightIcon } from "@heroicons/react/solid"; import { ArrowRightIcon } from "@heroicons/react/solid";
import MarkdownIt from "markdown-it";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { FormEvent, useRef, useState } from "react"; import { FormEvent, useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import turndown from "@calcom/lib/turndownService";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, ImageUploader, showToast, TextArea } from "@calcom/ui"; import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
import { Avatar } from "@calcom/ui"; import { Avatar } from "@calcom/ui";
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]"; import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
const md = new MarkdownIt("default", { html: true, breaks: true });
type FormData = { type FormData = {
bio: string; bio: string;
}; };
@@ -21,13 +25,10 @@ const UserProfile = (props: IUserProfileProps) => {
const { user } = props; const { user } = props;
const { t } = useLocale(); const { t } = useLocale();
const avatarRef = useRef<HTMLInputElement>(null!); const avatarRef = useRef<HTMLInputElement>(null!);
const bioRef = useRef<HTMLTextAreaElement>(null); const { setValue, handleSubmit, getValues } = useForm<FormData>({
const { defaultValues: { bio: user?.bio || "" },
register, });
setValue,
handleSubmit,
formState: { errors },
} = useForm<FormData>({ defaultValues: { bio: user?.bio || "" } });
const { data: eventTypes } = trpc.viewer.eventTypes.list.useQuery(); const { data: eventTypes } = trpc.viewer.eventTypes.list.useQuery();
const [imageSrc, setImageSrc] = useState<string>(user?.avatar || ""); const [imageSrc, setImageSrc] = useState<string>(user?.avatar || "");
const utils = trpc.useContext(); const utils = trpc.useContext();
@@ -138,25 +139,12 @@ const UserProfile = (props: IUserProfileProps) => {
</div> </div>
</div> </div>
<fieldset className="mt-8"> <fieldset className="mt-8">
<label htmlFor="bio" className="mb-2 block text-sm font-medium text-gray-700"> <Label className="mb-2 block text-sm font-medium text-gray-700">{t("about")}</Label>
{t("about")} <Editor
</label> getText={() => md.render(getValues("bio") || user?.bio || "")}
<TextArea setText={(value: string) => setValue("bio", turndown(value))}
{...register("bio", { required: true })} excludedToolbarItems={["blockType"]}
ref={bioRef}
name="bio"
id="bio"
className="mt-1 block h-[60px] w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-gray-500 focus:outline-none focus:ring-gray-500 sm:text-sm"
defaultValue={user?.bio || undefined}
onChange={(event) => {
setValue("bio", event.target.value);
}}
/> />
{errors.bio && (
<p data-testid="required" className="py-2 text-xs text-red-500">
{t("required")}
</p>
)}
<p className="mt-2 font-sans text-sm font-normal text-gray-600 dark:text-white"> <p className="mt-2 font-sans text-sm font-normal text-gray-600 dark:text-white">
{t("few_sentences_about_yourself")} {t("few_sentences_about_yourself")}
</p> </p>
@@ -1,10 +1,10 @@
import { Icon } from "@calcom/ui"; import { ShieldCheckIcon } from "@calcom/ui/components/icon";
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => { const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
return ( return (
<div className="mb-4 sm:flex sm:items-start"> <div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10"> <div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<Icon.ShieldCheckIcon className="h-6 w-6 text-white" /> <ShieldCheckIcon className="h-6 w-6 text-white" />
</div> </div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title"> <h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
@@ -2,7 +2,8 @@ import { useMutation } from "@tanstack/react-query";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Badge, Icon, showToast, Switch } from "@calcom/ui"; import { Badge, showToast, Switch } from "@calcom/ui";
import { FiArrowLeft } from "@calcom/ui/components/icon";
export function CalendarSwitch(props: { export function CalendarSwitch(props: {
type: string; type: string;
@@ -74,7 +75,7 @@ export function CalendarSwitch(props: {
/> />
{props.defaultSelected && ( {props.defaultSelected && (
<Badge variant="gray"> <Badge variant="gray">
<Icon.FiArrowLeft className="mr-1" /> {t("adding_events_to")} <FiArrowLeft className="mr-1" /> {t("adding_events_to")}
</Badge> </Badge>
)} )}
</div> </div>
+18 -4
View File
@@ -1,3 +1,4 @@
import MarkdownIt from "markdown-it";
import Link from "next/link"; import Link from "next/link";
import { TeamPageProps } from "pages/team/[slug]"; import { TeamPageProps } from "pages/team/[slug]";
@@ -6,6 +7,8 @@ import { Avatar } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
type TeamType = TeamPageProps["team"]; type TeamType = TeamPageProps["team"];
type MembersType = TeamType["members"]; type MembersType = TeamType["members"];
type MemberType = MembersType[number]; type MemberType = MembersType[number];
@@ -13,6 +16,8 @@ type MemberType = MembersType[number];
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => { const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
const { t } = useLocale(); const { t } = useLocale();
const isBioEmpty = !member.bio || !member.bio.replace("<p><br></p>", "").length;
return ( return (
<Link key={member.id} href={`/${member.username}`}> <Link key={member.id} href={`/${member.username}`}>
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full w-[90%] flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 "> <div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full w-[90%] flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 ">
@@ -23,9 +28,18 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
/> />
<section className="line-clamp-4 mt-2 w-full space-y-1"> <section className="line-clamp-4 mt-2 w-full space-y-1">
<p className="font-medium text-gray-900 dark:text-white">{member.name}</p> <p className="font-medium text-gray-900 dark:text-white">{member.name}</p>
<p className="line-clamp-3 overflow-ellipsis text-sm font-normal text-gray-500 dark:text-white"> <div className="line-clamp-3 overflow-ellipsis text-sm font-normal text-gray-500 dark:text-white">
{member.bio || t("user_from_team", { user: member.name, team: teamName })} {!isBioEmpty ? (
</p> <>
<div
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
/>
</>
) : (
t("user_from_team", { user: member.name, team: teamName })
)}
</div>
</section> </section>
</div> </div>
</Link> </Link>
@@ -46,7 +60,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
); );
}; };
const Team = ({ team }: TeamPageProps) => { const Team = ({ team }: Omit<TeamPageProps, "trpcState">) => {
return ( return (
<div> <div>
<Members members={team.members} teamName={team.name} /> <Members members={team.members} teamName={team.name} />
+2 -5
View File
@@ -1,7 +1,7 @@
import classNames from "classnames"; import classNames from "classnames";
import { APP_NAME, LOGO } from "@calcom/lib/constants"; import { APP_NAME, LOGO } from "@calcom/lib/constants";
import { Credits, HeadSeo } from "@calcom/ui"; import { HeadSeo } from "@calcom/ui";
import Loader from "@components/Loader"; import Loader from "@components/Loader";
@@ -35,10 +35,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
<div className="mx-2 rounded-md border border-gray-200 bg-white px-4 py-10 sm:px-10"> <div className="mx-2 rounded-md border border-gray-200 bg-white px-4 py-10 sm:px-10">
{props.children} {props.children}
</div> </div>
<div className="mt-8 text-center text-sm text-gray-600"> <div className="mt-8 text-center text-sm text-gray-600">{props.footerText}</div>
{props.footerText}
<Credits />
</div>
</div> </div>
</div> </div>
); );
+3 -3
View File
@@ -1,8 +1,8 @@
import classNames from "classnames"; import classNames from "classnames";
import React, { useState } from "react"; import { useState } from "react";
import { ControllerRenderProps } from "react-hook-form"; import { ControllerRenderProps } from "react-hook-form";
import { Icon } from "@calcom/ui"; import { FiEdit2 } from "@calcom/ui/components/icon";
const EditableHeading = function EditableHeading({ const EditableHeading = function EditableHeading({
value, value,
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
<label className="min-w-8 relative inline-block"> <label className="min-w-8 relative inline-block">
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value}&nbsp;</span> <span className="whitespace-pre text-xl tracking-normal text-transparent">{value}&nbsp;</span>
{!isEditing && isReady && ( {!isEditing && isReady && (
<Icon.FiEdit2 className="ml-1 inline h-4 w-4 align-top text-gray-700 group-hover:text-gray-500" /> <FiEdit2 className="ml-1 inline h-4 w-4 align-top text-gray-700 group-hover:text-gray-500" />
)} )}
<input <input
{...passThroughProps} {...passThroughProps}
+3 -2
View File
@@ -1,11 +1,12 @@
import { Icon, Tooltip } from "@calcom/ui"; import { Tooltip } from "@calcom/ui";
import { FiInfo } from "@calcom/ui/components/icon";
export default function InfoBadge({ content }: { content: string }) { export default function InfoBadge({ content }: { content: string }) {
return ( return (
<> <>
<Tooltip side="top" content={content}> <Tooltip side="top" content={content}>
<span title={content}> <span title={content}>
<Icon.FiInfo className="relative top-px left-1 right-1 mt-px h-4 w-4 text-gray-500" /> <FiInfo className="relative top-px left-1 right-1 mt-px h-4 w-4 text-gray-500" />
</span> </span>
</Tooltip> </Tooltip>
</> </>
@@ -11,7 +11,8 @@ import { User } from "@calcom/prisma/client";
import { TRPCClientErrorLike } from "@calcom/trpc/client"; import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { RouterOutputs, trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app"; import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Icon, Input, Label } from "@calcom/ui"; import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Input, Label } from "@calcom/ui";
import { FiCheck, FiEdit2, FiExternalLink, StarIconSolid } from "@calcom/ui/components/icon";
export enum UsernameChangeStatusEnum { export enum UsernameChangeStatusEnum {
UPGRADE = "UPGRADE", UPGRADE = "UPGRADE",
@@ -230,8 +231,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
isInputUsernamePremium ? "text-orange-400" : "", isInputUsernamePremium ? "text-orange-400" : "",
usernameIsAvailable ? "" : "" usernameIsAvailable ? "" : ""
)}> )}>
{isInputUsernamePremium ? <Icon.StarIconSolid className="mt-[2px] w-6" /> : <></>} {isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
{!isInputUsernamePremium && usernameIsAvailable ? <Icon.FiCheck className="mt-2 w-6" /> : <></>} {!isInputUsernamePremium && usernameIsAvailable ? <FiCheck className="mt-2 w-6" /> : <></>}
</span> </span>
</div> </div>
</div> </div>
@@ -249,7 +250,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<DialogContent> <DialogContent>
<div className="flex flex-row"> <div className="flex flex-row">
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]"> <div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<Icon.FiEdit2 className="m-auto h-6 w-6" /> <FiEdit2 className="m-auto h-6 w-6" />
</div> </div>
<div className="mb-4 w-full px-4 pt-1"> <div className="mb-4 w-full px-4 pt-1">
<DialogHeader title={t("confirm_username_change_dialog_title")} /> <DialogHeader title={t("confirm_username_change_dialog_title")} />
@@ -283,7 +284,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
data-testid="go-to-billing" data-testid="go-to-billing"
href={paymentLink}> href={paymentLink}>
<> <>
{t("go_to_stripe_billing")} <Icon.FiExternalLink className="ml-1 h-4 w-4" /> {t("go_to_stripe_billing")} <FiExternalLink className="ml-1 h-4 w-4" />
</> </>
</Button> </Button>
)} )}
@@ -7,17 +7,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { TRPCClientErrorLike } from "@calcom/trpc/client"; import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { AppRouter } from "@calcom/trpc/server/routers/_app"; import { AppRouter } from "@calcom/trpc/server/routers/_app";
import { import { Button, Dialog, DialogClose, DialogContent, DialogHeader, TextField } from "@calcom/ui";
Button, import { FiCheck, FiEdit2 } from "@calcom/ui/components/icon";
Dialog,
DialogClose,
DialogContent,
DialogHeader,
Icon,
Input,
Label,
TextField,
} from "@calcom/ui";
interface ICustomUsernameProps { interface ICustomUsernameProps {
currentUsername: string | undefined; currentUsername: string | undefined;
@@ -145,7 +136,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
{currentUsername !== inputUsernameValue && ( {currentUsername !== inputUsernameValue && (
<div className="absolute right-[2px] top-6 flex flex-row"> <div className="absolute right-[2px] top-6 flex flex-row">
<span className={classNames("mx-2 py-2")}> <span className={classNames("mx-2 py-2")}>
{usernameIsAvailable ? <Icon.FiCheck className="w-6" /> : <></>} {usernameIsAvailable ? <FiCheck className="w-6" /> : <></>}
</span> </span>
</div> </div>
)} )}
@@ -165,7 +156,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
<DialogContent> <DialogContent>
<div style={{ display: "flex", flexDirection: "row" }}> <div style={{ display: "flex", flexDirection: "row" }}>
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]"> <div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<Icon.FiEdit2 className="m-auto h-6 w-6" /> <FiEdit2 className="m-auto h-6 w-6" />
</div> </div>
<div className="mb-4 w-full px-4 pt-1"> <div className="mb-4 w-full px-4 pt-1">
<DialogHeader title={t("confirm_username_change_dialog_title")} /> <DialogHeader title={t("confirm_username_change_dialog_title")} />
@@ -2,7 +2,8 @@ import React from "react";
import { Props } from "react-select"; import { Props } from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon, Avatar } from "@calcom/ui"; import { Avatar } from "@calcom/ui";
import { FiX } from "@calcom/ui/components/icon";
import Select from "@components/ui/form/Select"; import Select from "@components/ui/form/Select";
@@ -48,7 +49,7 @@ export const CheckedSelect = ({
alt={option.label} alt={option.label}
/> />
{option.label} {option.label}
<Icon.FiX <FiX
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))} onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
className="float-right mt-0.5 h-5 w-5 cursor-pointer text-gray-500" className="float-right mt-0.5 h-5 w-5 cursor-pointer text-gray-500"
/> />
+2 -2
View File
@@ -3,7 +3,7 @@ import "react-calendar/dist/Calendar.css";
import "react-date-picker/dist/DatePicker.css"; import "react-date-picker/dist/DatePicker.css";
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle"; import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
import { Icon } from "@calcom/ui"; import { FiCalendar } from "@calcom/ui/components/icon";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
@@ -23,7 +23,7 @@ export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }
className className
)} )}
clearIcon={null} clearIcon={null}
calendarIcon={<Icon.FiCalendar className="h-5 w-5 text-gray-500" />} calendarIcon={<FiCalendar className="h-5 w-5 text-gray-500" />}
value={date} value={date}
minDate={minDate} minDate={minDate}
disabled={disabled} disabled={disabled}
-1
View File
@@ -1 +0,0 @@
export * from "@calcom/lib/availability";
-2
View File
@@ -1,2 +0,0 @@
// TODO: Remove this file once everything is imported from `@calcom/lib`
export * from "@calcom/lib/constants";
-1
View File
@@ -1 +0,0 @@
export * from "@calcom/lib/weekday";
-3
View File
@@ -1,3 +0,0 @@
/* Prefer import from `@calcom/lib/isOutOfBounds` */
export * from "@calcom/lib/isOutOfBounds";
export { default } from "@calcom/lib/isOutOfBounds";
-1
View File
@@ -1 +0,0 @@
export * from "@calcom/core/location";
-1
View File
@@ -1 +0,0 @@
export { default } from "@calcom/prisma";
-2
View File
@@ -1,2 +0,0 @@
// TODO: Remove this file once everything is imported from `@calcom/lib`
export * from "@calcom/lib/random";
-3
View File
@@ -1,3 +0,0 @@
/** Prefer import from `@calcom/lib/slots` */
export * from "@calcom/lib/slots";
export { default } from "@calcom/lib/slots";
-3
View File
@@ -1,3 +0,0 @@
// TODO: Remove this file once every `classNames` is imported from `@calcom/lib`
export * from "@calcom/lib/slugify";
export { default } from "@calcom/lib/slugify";
+14
View File
@@ -176,6 +176,20 @@ const nextConfig = {
}, */ }, */
]; ];
}, },
async headers() {
return [
{
// prettier-ignore
source: "/:path*((?<!\/embed$)(?<!\/embed\/preview\.html$))",
headers: [
{
key: "X-Frame-Options",
value: "DENY",
},
],
},
];
},
async redirects() { async redirects() {
const redirects = [ const redirects = [
{ {
+7 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "2.5.3", "version": "2.5.7",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
@@ -23,7 +23,7 @@
"yarn": ">=1.19.0 < 2.0.0" "yarn": ">=1.19.0 < 2.0.0"
}, },
"dependencies": { "dependencies": {
"@boxyhq/saml-jackson": "1.3.6", "@boxyhq/saml-jackson": "1.7.1",
"@calcom/app-store": "*", "@calcom/app-store": "*",
"@calcom/app-store-cli": "*", "@calcom/app-store-cli": "*",
"@calcom/core": "*", "@calcom/core": "*",
@@ -37,7 +37,7 @@
"@calcom/trpc": "*", "@calcom/trpc": "*",
"@calcom/tsconfig": "*", "@calcom/tsconfig": "*",
"@calcom/ui": "*", "@calcom/ui": "*",
"@daily-co/daily-js": "^0.26.0", "@daily-co/daily-js": "^0.37.0",
"@formkit/auto-animate": "^1.0.0-beta.5", "@formkit/auto-animate": "^1.0.0-beta.5",
"@glidejs/glide": "^3.5.2", "@glidejs/glide": "^3.5.2",
"@heroicons/react": "^1.0.6", "@heroicons/react": "^1.0.6",
@@ -60,6 +60,7 @@
"@stripe/react-stripe-js": "^1.10.0", "@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0", "@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9", "@tanstack/react-query": "^4.3.9",
"@types/turndown": "^5.0.1",
"@vercel/edge-functions-ui": "^0.2.1", "@vercel/edge-functions-ui": "^0.2.1",
"@vercel/og": "^0.0.21", "@vercel/og": "^0.0.21",
"accept-language-parser": "^1.5.0", "accept-language-parser": "^1.5.0",
@@ -73,6 +74,7 @@
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"ical.js": "^1.4.0", "ical.js": "^1.4.0",
"ics": "^2.37.0", "ics": "^2.37.0",
"jose": "^4.11.1",
"kbar": "^0.1.0-beta.36", "kbar": "^0.1.0-beta.36",
"libphonenumber-js": "^1.10.12", "libphonenumber-js": "^1.10.12",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@@ -118,6 +120,7 @@
"stripe": "^9.16.0", "stripe": "^9.16.0",
"superjson": "1.9.1", "superjson": "1.9.1",
"tailwindcss-radix": "^2.6.0", "tailwindcss-radix": "^2.6.0",
"turndown": "^7.1.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"web3": "^1.7.5", "web3": "^1.7.5",
"zod": "^3.20.2" "zod": "^3.20.2"
@@ -162,6 +165,6 @@
"tailwindcss": "^3.2.1", "tailwindcss": "^3.2.1",
"ts-jest": "^28.0.8", "ts-jest": "^28.0.8",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.7.4" "typescript": "^4.9.4"
} }
} }
+13 -12
View File
@@ -4,7 +4,8 @@ import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants"; import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { Icon, HeadSeo } from "@calcom/ui"; import { HeadSeo } from "@calcom/ui";
import { FiFileText, FiCheck, FiBookOpen, FiChevronRight } from "@calcom/ui/components/icon";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
@@ -20,13 +21,13 @@ export default function Custom404() {
{ {
title: t("documentation"), title: t("documentation"),
description: t("documentation_description"), description: t("documentation_description"),
icon: Icon.FiFileText, icon: FiFileText,
href: DOCS_URL, href: DOCS_URL,
}, },
{ {
title: t("blog"), title: t("blog"),
description: t("blog_description"), description: t("blog_description"),
icon: Icon.FiBookOpen, icon: FiBookOpen,
href: `${WEBSITE_URL}/blog`, href: `${WEBSITE_URL}/blog`,
}, },
]; ];
@@ -75,7 +76,7 @@ export default function Custom404() {
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50"> <span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
<Icon.FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" /> <FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" />
</span> </span>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@@ -90,7 +91,7 @@ export default function Custom404() {
<p className="text-base text-gray-500">{t("the_infrastructure_plan")}</p> <p className="text-base text-gray-500">{t("the_infrastructure_plan")}</p>
</div> </div>
<div className="flex-shrink-0 self-center"> <div className="flex-shrink-0 self-center">
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" /> <FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div> </div>
</a> </a>
</li> </li>
@@ -103,7 +104,7 @@ export default function Custom404() {
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50"> <span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
<Icon.FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" /> <FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" />
</span> </span>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@@ -116,7 +117,7 @@ export default function Custom404() {
<p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p> <p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p>
</div> </div>
<div className="flex-shrink-0 self-center"> <div className="flex-shrink-0 self-center">
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" /> <FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div> </div>
</Link> </Link>
</li> </li>
@@ -161,7 +162,7 @@ export default function Custom404() {
<p className="text-base text-gray-500">{t("join_our_community")}</p> <p className="text-base text-gray-500">{t("join_our_community")}</p>
</div> </div>
<div className="flex-shrink-0 self-center"> <div className="flex-shrink-0 self-center">
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" /> <FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div> </div>
</a> </a>
</li> </li>
@@ -218,7 +219,7 @@ export default function Custom404() {
rel="noreferrer"> rel="noreferrer">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50"> <span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
<Icon.FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" /> <FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" />
</span> </span>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@@ -233,7 +234,7 @@ export default function Custom404() {
<p className="text-base text-gray-500">{t("claim_username_and_schedule_events")}</p> <p className="text-base text-gray-500">{t("claim_username_and_schedule_events")}</p>
</div> </div>
<div className="flex-shrink-0 self-center"> <div className="flex-shrink-0 self-center">
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" /> <FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div> </div>
</a> </a>
</li> </li>
@@ -261,7 +262,7 @@ export default function Custom404() {
<p className="text-base text-gray-500">{link.description}</p> <p className="text-base text-gray-500">{link.description}</p>
</div> </div>
<div className="flex-shrink-0 self-center"> <div className="flex-shrink-0 self-center">
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" /> <FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div> </div>
</Link> </Link>
</li> </li>
@@ -307,7 +308,7 @@ export default function Custom404() {
<p className="text-base text-gray-500">{t("join_our_community")}</p> <p className="text-base text-gray-500">{t("join_our_community")}</p>
</div> </div>
<div className="flex-shrink-0 self-center"> <div className="flex-shrink-0 self-center">
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" /> <FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div> </div>
</a> </a>
</li> </li>
+3 -2
View File
@@ -3,7 +3,8 @@ import { useRouter } from "next/router";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants"; import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, showToast } from "@calcom/ui"; import { Button, showToast } from "@calcom/ui";
import { FiCopy } from "@calcom/ui/components/icon";
export default function Error500() { export default function Error500() {
const { t } = useLocale(); const { t } = useLocale();
@@ -33,7 +34,7 @@ export default function Error500() {
<Button <Button
color="secondary" color="secondary"
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300" className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
StartIcon={Icon.FiCopy} StartIcon={FiCopy}
onClick={() => { onClick={() => {
navigator.clipboard.writeText(router.query.error as string); navigator.clipboard.writeText(router.query.error as string);
showToast("Link copied!", "success"); showToast("Link copied!", "success");
+20 -7
View File
@@ -1,4 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import MarkdownIt from "markdown-it";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -26,13 +27,16 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects"; import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Icon, HeadSeo, AvatarGroup, Avatar } from "@calcom/ui"; import { HeadSeo, AvatarGroup, Avatar } from "@calcom/ui";
import { BadgeCheckIcon, FiArrowRight } from "@calcom/ui/components/icon";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr"; import { EmbedProps } from "@lib/withEmbedSsr";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) { export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props; const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group const [user] = users; //To be used when we only have a single user, not dynamic group
@@ -40,6 +44,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
const isBioEmpty = !user.bio || !user.bio.replace("<p><br></p>", "").length;
const groupEventTypes = props.users.some((user) => !user.allowDynamicBooking) ? ( const groupEventTypes = props.users.some((user) => !user.allowDynamicBooking) ? (
<div className="space-y-6" data-testid="event-types"> <div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900"> <div className="overflow-hidden rounded-sm border dark:border-gray-900">
@@ -54,8 +60,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
{eventTypes.map((type, index) => ( {eventTypes.map((type, index) => (
<li <li
key={index} key={index}
className="dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600"> className="dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600">
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" /> <FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
<Link <Link
href={getUsernameSlugLink({ users: props.users, slug: type.slug })} href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
className="flex justify-between px-6 py-4" className="flex justify-between px-6 py-4"
@@ -134,10 +140,17 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<h1 className="font-cal mb-1 text-3xl text-gray-900 dark:text-white"> <h1 className="font-cal mb-1 text-3xl text-gray-900 dark:text-white">
{nameOrUsername} {nameOrUsername}
{user.verified && ( {user.verified && (
<Icon.BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" /> <BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" />
)} )}
</h1> </h1>
<p className="dark:text-darkgray-600 text-s text-gray-500">{user.bio}</p> {!isBioEmpty && (
<>
<div
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(user.bio || "") }}
/>
</>
)}
</div> </div>
)} )}
<div <div
@@ -162,8 +175,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<div <div
key={type.id} key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }} style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
className="dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600"> className="dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600">
<Icon.FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" /> <FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */} {/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<Link <Link
prefetch={false} prefetch={false}
-1
View File
@@ -1,6 +1,5 @@
import MarkdownIt from "markdown-it"; import MarkdownIt from "markdown-it";
import { GetStaticPaths, GetStaticPropsContext } from "next"; import { GetStaticPaths, GetStaticPropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { z } from "zod"; import { z } from "zod";
import { privacyFilteredLocations, LocationObject } from "@calcom/app-store/locations"; import { privacyFilteredLocations, LocationObject } from "@calcom/app-store/locations";
-1
View File
@@ -1,5 +1,4 @@
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations"; import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
+45 -6
View File
@@ -1,6 +1,8 @@
import { IdentityProvider, UserPermissionRole } from "@prisma/client"; import { IdentityProvider, UserPermissionRole } from "@prisma/client";
import { BinaryLike, hkdfSync, KeyObject } from "crypto";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import Handlebars from "handlebars"; import Handlebars from "handlebars";
import * as jose from "jose";
import NextAuth, { Session } from "next-auth"; import NextAuth, { Session } from "next-auth";
import { Provider } from "next-auth/providers"; import { Provider } from "next-auth/providers";
import CredentialsProvider from "next-auth/providers/credentials"; import CredentialsProvider from "next-auth/providers/credentials";
@@ -9,6 +11,7 @@ import GoogleProvider from "next-auth/providers/google";
import nodemailer, { TransportOptions } from "nodemailer"; import nodemailer, { TransportOptions } from "nodemailer";
import { authenticator } from "otplib"; import { authenticator } from "otplib";
import path from "path"; import path from "path";
import { v4 as uuidv4 } from "uuid";
import checkLicense from "@calcom/features/ee/common/server/checkLicense"; import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider"; import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
@@ -17,14 +20,14 @@ import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto"; import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies"; import { defaultCookies } from "@calcom/lib/default-cookies";
import { randomString } from "@calcom/lib/random";
import rateLimit from "@calcom/lib/rateLimit"; import rateLimit from "@calcom/lib/rateLimit";
import { serverConfig } from "@calcom/lib/serverConfig"; import { serverConfig } from "@calcom/lib/serverConfig";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
import CalComAdapter from "@lib/auth/next-auth-custom-adapter"; import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
import { randomString } from "@lib/random";
import slugify from "@lib/slugify";
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants"; import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
@@ -60,6 +63,7 @@ const providers: Provider[] = [
username: true, username: true,
name: true, name: true,
email: true, email: true,
metadata: true,
identityProvider: true, identityProvider: true,
password: true, password: true,
twoFactorEnabled: true, twoFactorEnabled: true,
@@ -72,8 +76,9 @@ const providers: Provider[] = [
}, },
}); });
// Don't leak information about it being username or password that is invalid
if (!user) { if (!user) {
throw new Error(ErrorCode.UserNotFound); throw new Error(ErrorCode.IncorrectUsernamePassword);
} }
if (user.identityProvider !== IdentityProvider.CAL) { if (user.identityProvider !== IdentityProvider.CAL) {
@@ -81,12 +86,12 @@ const providers: Provider[] = [
} }
if (!user.password) { if (!user.password) {
throw new Error(ErrorCode.UserMissingPassword); throw new Error(ErrorCode.IncorrectUsernamePassword);
} }
const isCorrectPassword = await verifyPassword(credentials.password, user.password); const isCorrectPassword = await verifyPassword(credentials.password, user.password);
if (!isCorrectPassword) { if (!isCorrectPassword) {
throw new Error(ErrorCode.IncorrectPassword); throw new Error(ErrorCode.IncorrectUsernamePassword);
} }
if (user.twoFactorEnabled) { if (user.twoFactorEnabled) {
@@ -234,6 +239,9 @@ if (true) {
); );
} }
const calcomAdapter = CalComAdapter(prisma); const calcomAdapter = CalComAdapter(prisma);
const getDerivedEncryptionKey = async (secret: BinaryLike | KeyObject) => {
return await hkdfSync("sha256", secret, "", "NextAuth.js Generated Encryption Key", 32);
};
export default NextAuth({ export default NextAuth({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
@@ -241,6 +249,37 @@ export default NextAuth({
session: { session: {
strategy: "jwt", strategy: "jwt",
}, },
jwt: {
encode: async ({ secret, token }) => {
if (!token || token.sub === undefined) throw new Error("Not valid token");
const user = await prisma.user.findFirst({
where: { id: Number(token.sub) },
select: { metadata: true },
});
const encryptionSecret = await getDerivedEncryptionKey(secret);
const metadata = userMetadata.parse(user?.metadata);
return await new jose.EncryptJWT({
sub: token?.sub,
name: token?.name,
email: token?.email,
})
.setProtectedHeader({
alg: "dir",
enc: "A256GCM",
})
.setIssuedAt()
.setExpirationTime(metadata?.sessionTimeout ? `${metadata.sessionTimeout}m` : "30d")
.setJti(uuidv4())
.encrypt(new Uint8Array(encryptionSecret));
},
decode: async ({ secret, token }) => {
const encryptionSecret = await getDerivedEncryptionKey(secret);
const { payload } = await jose.jwtDecrypt(token || "", new Uint8Array(encryptionSecret), {
clockTolerance: 15,
});
return payload;
},
},
cookies: defaultCookies(WEBAPP_URL?.startsWith("https://")), cookies: defaultCookies(WEBAPP_URL?.startsWith("https://")),
pages: { pages: {
signIn: "/auth/login", signIn: "/auth/login",
+1 -2
View File
@@ -4,10 +4,9 @@ import { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { sendPasswordResetEmail } from "@calcom/emails"; import { sendPasswordResetEmail } from "@calcom/emails";
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email"; import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { getTranslation } from "@server/lib/i18n";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const t = await getTranslation(req.body.language ?? "en", "common"); const t = await getTranslation(req.body.language ?? "en", "common");
+37
View File
@@ -0,0 +1,37 @@
import { NextApiRequest, NextApiResponse } from "next";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { HttpError } from "@lib/core/http/error";
// This is the callback endpoint for the OIDC provider
// A team must set this endpoint in the OIDC provider's configuration
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") {
return res.status(400).send("Method not allowed");
}
const { code, state } = req.query as {
code: string;
state: string;
};
const { oauthController } = await jackson();
try {
const { redirect_url } = await oauthController.oidcAuthzResponse({ code, state });
if (!redirect_url) {
throw new HttpError({
message: "No redirect URL found",
statusCode: 500,
});
}
return res.redirect(302, redirect_url);
} catch (err) {
const { message, statusCode = 500 } = err as HttpError;
return res.status(statusCode).send(message);
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { IdentityProvider } from "@prisma/client"; import { IdentityProvider } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest } from "next";
import z from "zod"; import z from "zod";
import { isPasswordValid } from "@calcom/lib/auth"; import { isPasswordValid } from "@calcom/lib/auth";
@@ -50,7 +50,7 @@ async function handler(req: NextApiRequest) {
}, },
}); });
return { message: "First admin user created successfuly." }; return { message: "First admin user created successfully." };
} }
export default defaultHandler({ export default defaultHandler({
+1 -2
View File
@@ -2,11 +2,10 @@ import { IdentityProvider } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/lib/auth"; import { hashPassword } from "@calcom/lib/auth";
import slugify from "@calcom/lib/slugify";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager"; import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import slugify from "@lib/slugify";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") { if (req.method !== "POST") {
return; return;
+1 -2
View File
@@ -4,11 +4,10 @@ import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { sendOrganizerRequestReminderEmail } from "@calcom/emails"; import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar"; import type { CalendarEvent } from "@calcom/types/Calendar";
import { getTranslation } from "@server/lib/i18n";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey; const apiKey = req.headers.authorization || req.query.apiKey;
if (process.env.CRON_API_KEY !== apiKey) { if (process.env.CRON_API_KEY !== apiKey) {
@@ -19,6 +19,7 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
<Shell <Shell
isPublic isPublic
backPath="/apps" backPath="/apps"
smallHeading
heading={ heading={
<> <>
<Link <Link
@@ -33,8 +34,7 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
</span> </span>
)} )}
</> </>
} }>
large>
<div className="mb-16"> <div className="mb-16">
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3"> <div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
{apps.map((app) => { {apps.map((app) => {
+4 -3
View File
@@ -4,7 +4,8 @@ import Link from "next/link";
import { getAppRegistry } from "@calcom/app-store/_appRegistry"; import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon, SkeletonText } from "@calcom/ui"; import { SkeletonText } from "@calcom/ui";
import { FiArrowLeft, FiArrowRight } from "@calcom/ui/components/icon";
export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) { export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) {
const { t, isLocaleReady } = useLocale(); const { t, isLocaleReady } = useLocale();
@@ -15,7 +16,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
<Link <Link
href="/apps" href="/apps"
className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900"> className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
<Icon.FiArrowLeft className="h-4 w-4" /> <FiArrowLeft className="h-4 w-4" />
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "} {isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
</Link> </Link>
</div> </div>
@@ -31,7 +32,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
<h3 className="font-medium capitalize">{category.name}</h3> <h3 className="font-medium capitalize">{category.name}</h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{t("number_apps", { count: category.count })}{" "} {t("number_apps", { count: category.count })}{" "}
<Icon.FiArrowRight className="inline-block h-4 w-4" /> <FiArrowRight className="inline-block h-4 w-4" />
</p> </p>
</div> </div>
</Link> </Link>
+2 -2
View File
@@ -12,10 +12,10 @@ import {
AppStoreCategories, AppStoreCategories,
HorizontalTabItemProps, HorizontalTabItemProps,
HorizontalTabs, HorizontalTabs,
Icon,
TextField, TextField,
PopularAppsSlider, PopularAppsSlider,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiSearch } from "@calcom/ui/components/icon";
import AppsLayout from "@components/apps/layouts/AppsLayout"; import AppsLayout from "@components/apps/layouts/AppsLayout";
@@ -42,7 +42,7 @@ function AppsSearch({
return ( return (
<TextField <TextField
className="!border-gray-100 bg-gray-100 !pl-0 focus:!ring-offset-0" className="!border-gray-100 bg-gray-100 !pl-0 focus:!ring-offset-0"
addOnLeading={<Icon.FiSearch className="h-4 w-4 text-gray-500" />} addOnLeading={<FiSearch className="h-4 w-4 text-gray-500" />}
addOnClassname="!border-gray-100" addOnClassname="!border-gray-100"
containerClassName={classNames("focus:!ring-offset-0", className)} containerClassName={classNames("focus:!ring-offset-0", className)}
type="search" type="search"
+108 -59
View File
@@ -1,10 +1,11 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useReducer } from "react";
import z from "zod"; import z from "zod";
import { AppSettings } from "@calcom/app-store/_components/AppSettings"; import { AppSettings } from "@calcom/app-store/_components/AppSettings";
import { InstallAppButton } from "@calcom/app-store/components"; import { InstallAppButton } from "@calcom/app-store/components";
import { InstalledAppVariants } from "@calcom/app-store/utils"; import { InstalledAppVariants } from "@calcom/app-store/utils";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration"; import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { App } from "@calcom/types/App"; import { App } from "@calcom/types/App";
@@ -13,16 +14,31 @@ import {
Alert, Alert,
Button, Button,
EmptyScreen, EmptyScreen,
Icon,
List, List,
AppSkeletonLoader as SkeletonLoader, AppSkeletonLoader as SkeletonLoader,
ShellSubHeading, ShellSubHeading,
DropdownMenuTrigger,
DropdownMenuContent,
Dropdown,
DropdownMenuItem,
DropdownItem,
} from "@calcom/ui"; } from "@calcom/ui";
import {
FiBarChart,
FiCalendar,
FiCreditCard,
FiGrid,
FiMoreHorizontal,
FiPlus,
FiShare2,
FiTrash,
FiVideo,
} from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
import AppListCard from "@components/AppListCard";
import { CalendarListContainer } from "@components/apps/CalendarListContainer"; import { CalendarListContainer } from "@components/apps/CalendarListContainer";
import IntegrationListItem from "@components/apps/IntegrationListItem";
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout"; import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
function ConnectOrDisconnectIntegrationButton(props: { function ConnectOrDisconnectIntegrationButton(props: {
@@ -31,8 +47,9 @@ function ConnectOrDisconnectIntegrationButton(props: {
isGlobal?: boolean; isGlobal?: boolean;
installed?: boolean; installed?: boolean;
invalidCredentialIds?: number[]; invalidCredentialIds?: number[];
handleDisconnect: (credentialId: number) => void;
}) { }) {
const { type, credentialIds, isGlobal, installed } = props; const { type, credentialIds, isGlobal, installed, handleDisconnect } = props;
const { t } = useLocale(); const { t } = useLocale();
const [credentialId] = credentialIds; const [credentialId] = credentialIds;
@@ -41,25 +58,24 @@ function ConnectOrDisconnectIntegrationButton(props: {
utils.viewer.integrations.invalidate(); utils.viewer.integrations.invalidate();
}; };
if (credentialId) { if (credentialId || type === "stripe_payment" || isGlobal) {
if (type === "stripe_payment") {
return (
<DisconnectIntegration
credentialId={credentialId}
trashIcon
onSuccess={handleOpenChange}
buttonProps={{ className: "border border-gray-300" }}
/>
);
}
return ( return (
<DisconnectIntegration <Dropdown modal={false}>
credentialId={credentialId} <DropdownMenuTrigger asChild>
trashIcon <Button StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
onSuccess={handleOpenChange} </DropdownMenuTrigger>
buttonProps={{ className: "border border-gray-300" }} <DropdownMenuContent>
/> <DropdownMenuItem>
<DropdownItem
color="destructive"
onClick={() => handleDisconnect(credentialId)}
disabled={isGlobal}
StartIcon={FiTrash}>
{t("remove_app")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
); );
} }
@@ -70,14 +86,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
</div> </div>
); );
} }
/** We don't need to "Connect", just show that it's installed */
if (isGlobal) {
return (
<div className="truncate px-3 py-2">
<h3 className="text-sm font-medium text-gray-700">{t("default")}</h3>
</div>
);
}
return ( return (
<InstallAppButton <InstallAppButton
type={type} type={type}
@@ -94,58 +103,64 @@ function ConnectOrDisconnectIntegrationButton(props: {
interface IntegrationsContainerProps { interface IntegrationsContainerProps {
variant?: typeof InstalledAppVariants[number]; variant?: typeof InstalledAppVariants[number];
exclude?: typeof InstalledAppVariants[number][]; exclude?: typeof InstalledAppVariants[number][];
handleDisconnect: (credentialId: number) => void;
} }
interface IntegrationsListProps { interface IntegrationsListProps {
variant?: IntegrationsContainerProps["variant"]; variant?: IntegrationsContainerProps["variant"];
data: RouterOutputs["viewer"]["integrations"]; data: RouterOutputs["viewer"]["integrations"];
handleDisconnect: (credentialId: number) => void;
} }
const IntegrationsList = ({ data }: IntegrationsListProps) => { const IntegrationsList = ({ data, handleDisconnect }: IntegrationsListProps) => {
return ( return (
<List className="flex flex-col gap-6" noBorderTreatment> <List>
{data.items {data.items
.filter((item) => item.invalidCredentialIds) .filter((item) => item.invalidCredentialIds)
.map((item) => ( .map((item) => (
<IntegrationListItem <AppListCard
name={item.name} key={item.name}
slug={item.slug}
key={item.title}
title={item.title}
logo={item.logo}
description={item.description} description={item.description}
separate={true} title={item.name}
isTemplate={item.isTemplate} logo={item.logo}
isDefault={item.isGlobal}
shouldHighlight
slug={item.slug}
invalidCredential={item.invalidCredentialIds.length > 0} invalidCredential={item.invalidCredentialIds.length > 0}
actions={ actions={
<div className="flex w-16 justify-end"> <div className="flex justify-end">
<ConnectOrDisconnectIntegrationButton <ConnectOrDisconnectIntegrationButton
credentialIds={item.credentialIds} credentialIds={item.credentialIds}
type={item.type} type={item.type}
isGlobal={item.isGlobal} isGlobal={item.isGlobal}
installed installed
invalidCredentialIds={item.invalidCredentialIds} invalidCredentialIds={item.invalidCredentialIds}
handleDisconnect={handleDisconnect}
/> />
</div> </div>
}> }>
<AppSettings slug={item.slug} /> <AppSettings slug={item.slug} />
</IntegrationListItem> </AppListCard>
))} ))}
</List> </List>
); );
}; };
const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps): JSX.Element => { const IntegrationsContainer = ({
variant,
exclude,
handleDisconnect,
}: IntegrationsContainerProps): JSX.Element => {
const { t } = useLocale(); const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant, exclude, onlyInstalled: true }); const query = trpc.viewer.integrations.useQuery({ variant, exclude, onlyInstalled: true });
const emptyIcon = { const emptyIcon = {
calendar: Icon.FiCalendar, calendar: FiCalendar,
conferencing: Icon.FiVideo, conferencing: FiVideo,
automation: Icon.FiShare2, automation: FiShare2,
analytics: Icon.FiBarChart, analytics: FiBarChart,
payment: Icon.FiCreditCard, payment: FiCreditCard,
web3: Icon.FiBarChart, web3: FiBarChart,
other: Icon.FiGrid, other: FiGrid,
}; };
return ( return (
@@ -169,12 +184,12 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
: "/apps" : "/apps"
} }
color="secondary" color="secondary"
StartIcon={Icon.FiPlus}> StartIcon={FiPlus}>
{t("add")} {t("add")}
</Button> </Button>
} }
/> />
<IntegrationsList data={data} variant={variant} /> <IntegrationsList handleDisconnect={handleDisconnect} data={data} variant={variant} />
</div> </div>
) : ( ) : (
<EmptyScreen <EmptyScreen
@@ -206,6 +221,11 @@ const querySchema = z.object({
type querySchemaType = z.infer<typeof querySchema>; type querySchemaType = z.infer<typeof querySchema>;
type ModalState = {
isOpen: boolean;
credentialId: null | number;
};
export default function InstalledApps() { export default function InstalledApps() {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
@@ -218,14 +238,43 @@ export default function InstalledApps() {
"web3", "web3",
]; ];
const [data, updateData] = useReducer(
(data: ModalState, partialData: Partial<ModalState>) => ({ ...data, ...partialData }),
{
isOpen: false,
credentialId: null,
}
);
const handleModelClose = () => {
updateData({ isOpen: false, credentialId: null });
};
const handleDisconnect = (credentialId: number) => {
updateData({ isOpen: true, credentialId });
};
return ( return (
<InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}> <>
{categoryList.includes(category) && <IntegrationsContainer variant={category} />} <InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
{category === "calendar" && <CalendarListContainer />} {categoryList.includes(category) && (
{category === "other" && ( <IntegrationsContainer handleDisconnect={handleDisconnect} variant={category} />
<IntegrationsContainer variant={category} exclude={[...categoryList, "calendar"]} /> )}
)} {category === "calendar" && <CalendarListContainer />}
</InstalledAppsLayout> {category === "other" && (
<IntegrationsContainer
handleDisconnect={handleDisconnect}
variant={category}
exclude={[...categoryList, "calendar"]}
/>
)}
</InstalledAppsLayout>
<DisconnectIntegrationModal
handleModelClose={handleModelClose}
isOpen={data.isOpen}
credentialId={data.credentialId}
/>
</>
); );
} }
+3 -2
View File
@@ -4,7 +4,8 @@ import { useRouter } from "next/router";
import z from "zod"; import z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, SkeletonText } from "@calcom/ui"; import { Button, SkeletonText } from "@calcom/ui";
import { FiX } from "@calcom/ui/components/icon";
import AuthContainer from "@components/ui/AuthContainer"; import AuthContainer from "@components/ui/AuthContainer";
@@ -28,7 +29,7 @@ export default function Error() {
<AuthContainer title="" description=""> <AuthContainer title="" description="">
<div> <div>
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100"> <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<Icon.FiX className="h-6 w-6 text-red-600" /> <FiX className="h-6 w-6 text-red-600" />
</div> </div>
<div className="mt-3 text-center sm:mt-5"> <div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title"> <h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
+8 -7
View File
@@ -9,14 +9,15 @@ import { FaGoogle } from "react-icons/fa";
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin"; import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { ErrorCode, getSession } from "@calcom/lib/auth";
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { Alert, Button, EmailField, Icon, PasswordField } from "@calcom/ui"; import { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
import { FiArrowLeft } from "@calcom/ui/components/icon";
import { ErrorCode, getSession } from "@lib/auth";
import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import AddToHomescreen from "@components/AddToHomescreen"; import AddToHomescreen from "@components/AddToHomescreen";
@@ -51,8 +52,8 @@ export default function Login({
const errorMessages: { [key: string]: string } = { const errorMessages: { [key: string]: string } = {
// [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"), // [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
[ErrorCode.IncorrectPassword]: `${t("incorrect_password")} ${t("please_try_again")}`, // Don't leak information about whether an email is registered or not
[ErrorCode.UserNotFound]: t("no_account_exists"), [ErrorCode.IncorrectUsernamePassword]: t("incorrect_username_password"),
[ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`, [ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`,
[ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`, [ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"), [ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
@@ -85,7 +86,7 @@ export default function Login({
setTwoFactorRequired(false); setTwoFactorRequired(false);
methods.setValue("totpCode", ""); methods.setValue("totpCode", "");
}} }}
StartIcon={Icon.FiArrowLeft} StartIcon={FiArrowLeft}
color="minimal"> color="minimal">
{t("go_back")} {t("go_back")}
</Button> </Button>
@@ -148,7 +149,7 @@ export default function Login({
</div> </div>
<PasswordField <PasswordField
id="password" id="password"
autoComplete="current-password" autoComplete="off"
required required
className="mb-0" className="mb-0"
{...register("password")} {...register("password")}
+3 -2
View File
@@ -5,7 +5,8 @@ import { useEffect } from "react";
import { WEBSITE_URL } from "@calcom/lib/constants"; import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon } from "@calcom/ui"; import { Button } from "@calcom/ui";
import { FiCheck } from "@calcom/ui/components/icon";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -31,7 +32,7 @@ export default function Logout(props: Props) {
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo> <AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
<div className="mb-4"> <div className="mb-4">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100"> <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
<Icon.FiCheck className="h-6 w-6 text-green-600" /> <FiCheck className="h-6 w-6 text-green-600" />
</div> </div>
<div className="mt-3 text-center sm:mt-5"> <div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title"> <h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
+2 -2
View File
@@ -1,7 +1,7 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui"; import { FiCheck } from "@calcom/ui/components/icon";
const StepDone = () => { const StepDone = () => {
const router = useRouter(); const router = useRouter();
@@ -18,7 +18,7 @@ const StepDone = () => {
}}> }}>
<div className="min-h-36 my-6 flex flex-col items-center justify-center"> <div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white"> <div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" /> <FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
</div> </div>
<div className="max-w-[420px] text-center"> <div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2> <h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
+3 -3
View File
@@ -15,7 +15,6 @@ import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/
import { import {
Button, Button,
Form, Form,
Icon,
Label, Label,
showToast, showToast,
Skeleton, Skeleton,
@@ -25,6 +24,7 @@ import {
Tooltip, Tooltip,
VerticalDivider, VerticalDivider,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiInfo, FiPlus } from "@calcom/ui/components/icon";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
@@ -56,7 +56,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
{t("date_overrides")}{" "} {t("date_overrides")}{" "}
<Tooltip content={t("date_overrides_info")}> <Tooltip content={t("date_overrides_info")}>
<span className="inline-block"> <span className="inline-block">
<Icon.FiInfo /> <FiInfo />
</span> </span>
</Tooltip> </Tooltip>
</h3> </h3>
@@ -74,7 +74,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))} excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
onChange={(ranges) => append({ ranges })} onChange={(ranges) => append({ ranges })}
Trigger={ Trigger={
<Button color="secondary" StartIcon={Icon.FiPlus} data-testid="add-override"> <Button color="secondary" StartIcon={FiPlus} data-testid="add-override">
Add an override Add an override
</Button> </Button>
} }
+3 -2
View File
@@ -5,7 +5,8 @@ import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules"
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { EmptyScreen, Icon, showToast } from "@calcom/ui"; import { EmptyScreen, showToast } from "@calcom/ui";
import { FiClock } from "@calcom/ui/components/icon";
import { withQuery } from "@lib/QueryCell"; import { withQuery } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
@@ -76,7 +77,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
{schedules.length === 0 ? ( {schedules.length === 0 ? (
<div className="flex justify-center"> <div className="flex justify-center">
<EmptyScreen <EmptyScreen
Icon={Icon.FiClock} Icon={FiClock}
headline={t("new_schedule_heading")} headline={t("new_schedule_heading")}
description={t("new_schedule_description")} description={t("new_schedule_description")}
buttonRaw={<NewScheduleButton />} buttonRaw={<NewScheduleButton />}
+12 -9
View File
@@ -35,7 +35,8 @@ import { localStorage } from "@calcom/lib/webstorage";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client"; import { Prisma } from "@calcom/prisma/client";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Button, EmailInput, Icon, HeadSeo } from "@calcom/ui"; import { Button, EmailInput, HeadSeo } from "@calcom/ui";
import { FiX, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock"; import { timeZone } from "@lib/clock";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -142,7 +143,7 @@ function RedirectionToast({ url }: { url: string }) {
setIsToastVisible(false); setIsToastVisible(false);
}} }}
className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white"> className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white">
<Icon.FiX className="h-6 w-6 text-white" /> <FiX className="h-6 w-6 text-white" />
</button> </button>
</div> </div>
</div> </div>
@@ -168,6 +169,7 @@ const querySchema = z.object({
reschedule: stringToBoolean, reschedule: stringToBoolean,
isSuccessBookingPage: z.string().optional(), isSuccessBookingPage: z.string().optional(),
formerTime: z.string().optional(), formerTime: z.string().optional(),
email: z.string().optional(),
}); });
export default function Success(props: SuccessProps) { export default function Success(props: SuccessProps) {
@@ -179,6 +181,7 @@ export default function Success(props: SuccessProps) {
cancel: isCancellationMode, cancel: isCancellationMode,
changes, changes,
formerTime, formerTime,
email,
} = querySchema.parse(router.query); } = querySchema.parse(router.query);
if ((isCancellationMode || changes) && typeof window !== "undefined") { if ((isCancellationMode || changes) && typeof window !== "undefined") {
@@ -195,7 +198,6 @@ export default function Success(props: SuccessProps) {
console.error(`No location found `); console.error(`No location found `);
} }
const email = props.bookingInfo?.user?.email;
const status = props.bookingInfo?.status; const status = props.bookingInfo?.status;
const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED; const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED;
const cancellationReason = props.bookingInfo.cancellationReason; const cancellationReason = props.bookingInfo.cancellationReason;
@@ -238,7 +240,7 @@ export default function Success(props: SuccessProps) {
const giphyAppData = getEventTypeAppData(eventType, "giphy"); const giphyAppData = getEventTypeAppData(eventType, "giphy");
const giphyImage = giphyAppData?.thankYouPage; const giphyImage = giphyAppData?.thankYouPage;
const eventName = getEventName(eventNameObject, true); const eventName = getEventName(eventNameObject);
const needsConfirmation = eventType.requiresConfirmation && reschedule != true; const needsConfirmation = eventType.requiresConfirmation && reschedule != true;
const isCancelled = status === "CANCELLED" || status === "REJECTED"; const isCancelled = status === "CANCELLED" || status === "REJECTED";
const telemetry = useTelemetry(); const telemetry = useTelemetry();
@@ -254,6 +256,7 @@ export default function Success(props: SuccessProps) {
if (!sdkActionManager) return; if (!sdkActionManager) return;
// TODO: We should probably make it consistent with Webhook payload. Some data is not available here, as and when requirement comes we can add // TODO: We should probably make it consistent with Webhook payload. Some data is not available here, as and when requirement comes we can add
sdkActionManager.fire("bookingSuccessful", { sdkActionManager.fire("bookingSuccessful", {
booking: bookingInfo,
eventType, eventType,
date: date.toString(), date: date.toString(),
duration: calculatedDuration, duration: calculatedDuration,
@@ -356,7 +359,7 @@ export default function Success(props: SuccessProps) {
<Link <Link
href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"} href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}
className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-transparent dark:hover:text-white"> className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-transparent dark:hover:text-white">
<Icon.FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")} <FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
</Link> </Link>
</div> </div>
)} )}
@@ -401,10 +404,10 @@ export default function Success(props: SuccessProps) {
<img src={giphyImage} alt="Gif from Giphy" /> <img src={giphyImage} alt="Gif from Giphy" />
)} )}
{!giphyImage && !needsConfirmation && !isCancelled && ( {!giphyImage && !needsConfirmation && !isCancelled && (
<Icon.FiCheck className="h-5 w-5 text-green-600" /> <FiCheck className="h-5 w-5 text-green-600" />
)} )}
{needsConfirmation && !isCancelled && <Icon.FiCalendar className="h-5 w-5 text-gray-900" />} {needsConfirmation && !isCancelled && <FiCalendar className="h-5 w-5 text-gray-900" />}
{isCancelled && <Icon.FiX className="h-5 w-5 text-red-600" />} {isCancelled && <FiX className="h-5 w-5 text-red-600" />}
</div> </div>
<div className="mt-6 mb-8 text-center last:mb-0"> <div className="mt-6 mb-8 text-center last:mb-0">
<h3 <h3
@@ -733,7 +736,7 @@ export default function Success(props: SuccessProps) {
<EmailInput <EmailInput
name="email" name="email"
id="email" id="email"
defaultValue={email || ""} defaultValue={email}
className="mr- focus:border-brand border-bookinglightest dark:border-darkgray-300 mt-0 block w-full rounded-none rounded-l-md border-gray-300 shadow-sm focus:ring-black dark:bg-black dark:text-white sm:text-sm" className="mr- focus:border-brand border-bookinglightest dark:border-darkgray-300 mt-0 block w-full rounded-none rounded-l-md border-gray-300 shadow-sm focus:ring-black dark:bg-black dark:text-white sm:text-sm"
placeholder="rick.astley@cal.com" placeholder="rick.astley@cal.com"
/> />
+3 -2
View File
@@ -9,7 +9,8 @@ import BookingLayout from "@calcom/features/bookings/layout/BookingLayout";
import { filterQuerySchema, useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery"; import { filterQuerySchema, useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Alert, Button, EmptyScreen, Icon } from "@calcom/ui"; import { Alert, Button, EmptyScreen } from "@calcom/ui";
import { FiCalendar } from "@calcom/ui/components/icon";
import { useInViewObserver } from "@lib/hooks/useInViewObserver"; import { useInViewObserver } from "@lib/hooks/useInViewObserver";
@@ -176,7 +177,7 @@ export default function Bookings() {
{query.status === "success" && isEmpty && ( {query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:pt-0"> <div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen <EmptyScreen
Icon={Icon.FiCalendar} Icon={FiCalendar}
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })} headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", { description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(), status: t(status).toLowerCase(),
+1 -1
View File
@@ -3,11 +3,11 @@ import { z } from "zod";
import { privacyFilteredLocations, LocationObject } from "@calcom/core/location"; import { privacyFilteredLocations, LocationObject } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import { getWorkingHours } from "@calcom/lib/availability";
import { availiblityPageEventTypeSelect } from "@calcom/prisma"; import { availiblityPageEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { getWorkingHours } from "@lib/availability";
import { GetBookingType } from "@lib/getBooking"; import { GetBookingType } from "@lib/getBooking";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr"; import { EmbedProps } from "@lib/withEmbedSsr";
-1
View File
@@ -1,5 +1,4 @@
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
@@ -10,8 +10,6 @@ import { z } from "zod";
import { EventLocationType } from "@calcom/core/location"; import { EventLocationType } from "@calcom/core/location";
import { validateBookingLimitOrder } from "@calcom/lib"; import { validateBookingLimitOrder } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
import findDurationType from "@calcom/lib/findDurationType";
import getEventTypeById from "@calcom/lib/getEventTypeById"; import getEventTypeById from "@calcom/lib/getEventTypeById";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
+124 -58
View File
@@ -2,7 +2,8 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { Fragment, useEffect, useState } from "react"; import { FC, useEffect, useState, memo } from "react";
import { z } from "zod";
import { import {
CreateEventTypeButton, CreateEventTypeButton,
@@ -11,8 +12,12 @@ import {
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react"; import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
import { import {
Avatar,
AvatarGroup,
Badge, Badge,
Button, Button,
ButtonGroup, ButtonGroup,
@@ -26,13 +31,26 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
EmptyScreen, EmptyScreen,
Icon,
showToast, showToast,
Switch, Switch,
Avatar,
AvatarGroup,
Tooltip, Tooltip,
HorizontalTabs,
} from "@calcom/ui"; } from "@calcom/ui";
import {
FiArrowDown,
FiArrowUp,
FiClipboard,
FiCode,
FiCopy,
FiEdit,
FiEdit2,
FiExternalLink,
FiLink,
FiMoreHorizontal,
FiTrash,
FiUpload,
FiUsers,
} from "@calcom/ui/components/icon";
import { withQuery } from "@lib/QueryCell"; import { withQuery } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
@@ -60,6 +78,40 @@ interface EventTypeListProps {
types: EventType[]; types: EventType[];
} }
interface MobileTeamsTabProps {
eventTypeGroups: EventTypeGroups;
}
const querySchema = z.object({
teamId: z.nullable(z.coerce.number()).optional().default(null),
});
const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
const { eventTypeGroups } = props;
const tabs = eventTypeGroups.map((item) => ({
name: item.profile.name ?? "",
href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types",
avatar: item.profile.image ?? `${WEBAPP_URL}/${item.profile.slug}/avatar.png`,
}));
const { data } = useTypedQuery(querySchema);
const events = eventTypeGroups.filter((item) => item.teamId === data.teamId);
return (
<div>
<HorizontalTabs tabs={tabs} />
{events.length && (
<EventTypeList
types={events[0].eventTypes}
group={events[0]}
groupIndex={0}
readOnly={events[0].metadata.readOnly}
/>
)}
</div>
);
};
const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => { const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => {
const { t } = useLocale(); const { t } = useLocale();
@@ -83,12 +135,15 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
</span> </span>
)} )}
</div> </div>
<EventTypeDescription eventType={type} /> <EventTypeDescription
// @ts-expect-error FIXME We have a type mismtach here @hariombalhara @sean-brydon
eventType={type}
/>
</Link> </Link>
); );
}; };
const MemoizedItem = React.memo(Item); const MemoizedItem = memo(Item);
export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => { export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => {
const { t } = useLocale(); const { t } = useLocale();
@@ -178,7 +233,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
} }
// inject selection data into url for correct router history // inject selection data into url for correct router history
const openDuplicateModal = (eventType: EventType) => { const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => {
const query = { const query = {
...router.query, ...router.query,
dialog: "duplicate-event-type", dialog: "duplicate-event-type",
@@ -187,6 +242,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
slug: eventType.slug, slug: eventType.slug,
id: eventType.id, id: eventType.id,
length: eventType.length, length: eventType.length,
pageSlug: group.profile.slug,
}; };
router.push( router.push(
@@ -263,7 +319,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<button <button
className="invisible absolute left-[5px] -mt-4 mb-4 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow disabled:hover:border-inherit disabled:hover:text-gray-400 disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]" className="invisible absolute left-[5px] -mt-4 mb-4 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow disabled:hover:border-inherit disabled:hover:text-gray-400 disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
onClick={() => moveEventType(index, -1)}> onClick={() => moveEventType(index, -1)}>
<Icon.FiArrowUp className="h-5 w-5" /> <FiArrowUp className="h-5 w-5" />
</button> </button>
)} )}
@@ -271,7 +327,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<button <button
className="invisible absolute left-[5px] mt-8 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow disabled:hover:border-inherit disabled:hover:text-gray-400 disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]" className="invisible absolute left-[5px] mt-8 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow disabled:hover:border-inherit disabled:hover:text-gray-400 disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
onClick={() => moveEventType(index, 1)}> onClick={() => moveEventType(index, 1)}>
<Icon.FiArrowDown className="h-5 w-5" /> <FiArrowDown className="h-5 w-5" />
</button> </button>
)} )}
<MemoizedItem type={type} group={group} readOnly={readOnly} /> <MemoizedItem type={type} group={group} readOnly={readOnly} />
@@ -314,7 +370,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
target="_blank" target="_blank"
variant="icon" variant="icon"
href={calLink} href={calLink}
StartIcon={Icon.FiExternalLink} StartIcon={FiExternalLink}
/> />
</Tooltip> </Tooltip>
@@ -322,7 +378,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<Button <Button
color="secondary" color="secondary"
variant="icon" variant="icon"
StartIcon={Icon.FiLink} StartIcon={FiLink}
onClick={() => { onClick={() => {
showToast(t("link_copied"), "success"); showToast(t("link_copied"), "success");
navigator.clipboard.writeText(calLink); navigator.clipboard.writeText(calLink);
@@ -338,7 +394,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
type="button" type="button"
variant="icon" variant="icon"
color="secondary" color="secondary"
StartIcon={Icon.FiMoreHorizontal} StartIcon={FiMoreHorizontal}
/> />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
@@ -346,7 +402,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownItem <DropdownItem
type="button" type="button"
data-testid={"event-type-edit-" + type.id} data-testid={"event-type-edit-" + type.id}
StartIcon={Icon.FiEdit2} StartIcon={FiEdit2}
onClick={() => router.push("/event-types/" + type.id)}> onClick={() => router.push("/event-types/" + type.id)}>
{t("edit")} {t("edit")}
</DropdownItem> </DropdownItem>
@@ -355,8 +411,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownItem <DropdownItem
type="button" type="button"
data-testid={"event-type-duplicate-" + type.id} data-testid={"event-type-duplicate-" + type.id}
StartIcon={Icon.FiCopy} StartIcon={FiCopy}
onClick={() => openDuplicateModal(type)}> onClick={() => openDuplicateModal(type, group)}>
{t("duplicate")} {t("duplicate")}
</DropdownItem> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
@@ -364,7 +420,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<EmbedButton <EmbedButton
as={DropdownItem} as={DropdownItem}
type="button" type="button"
StartIcon={Icon.FiCode} StartIcon={FiCode}
className="w-full rounded-none" className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}> embedUrl={encodeURIComponent(embedLink)}>
{t("embed")} {t("embed")}
@@ -380,7 +436,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id); setDeleteDialogTypeId(type.id);
}} }}
StartIcon={Icon.FiTrash} StartIcon={FiTrash}
className="w-full rounded-none"> className="w-full rounded-none">
{t("delete")} {t("delete")}
</DropdownItem> </DropdownItem>
@@ -396,12 +452,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<div className="min-w-9 mx-5 flex sm:hidden"> <div className="min-w-9 mx-5 flex sm:hidden">
<Dropdown> <Dropdown>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}> <DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
<Button <Button type="button" variant="icon" color="secondary" StartIcon={FiMoreHorizontal} />
type="button"
variant="icon"
color="secondary"
StartIcon={Icon.FiMoreHorizontal}
/>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuPortal> <DropdownMenuPortal>
<DropdownMenuContent> <DropdownMenuContent>
@@ -409,7 +460,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<Link href={calLink} target="_blank"> <Link href={calLink} target="_blank">
<Button <Button
color="minimal" color="minimal"
StartIcon={Icon.FiExternalLink} StartIcon={FiExternalLink}
className="w-full rounded-none"> className="w-full rounded-none">
{t("preview")} {t("preview")}
</Button> </Button>
@@ -421,7 +472,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
color="minimal" color="minimal"
className="w-full rounded-none text-left" className="w-full rounded-none text-left"
data-testid={"event-type-duplicate-" + type.id} data-testid={"event-type-duplicate-" + type.id}
StartIcon={Icon.FiClipboard} StartIcon={FiClipboard}
onClick={() => { onClick={() => {
navigator.clipboard.writeText(calLink); navigator.clipboard.writeText(calLink);
showToast(t("link_copied"), "success"); showToast(t("link_copied"), "success");
@@ -436,7 +487,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
color="minimal" color="minimal"
className="w-full rounded-none" className="w-full rounded-none"
data-testid={"event-type-duplicate-" + type.id} data-testid={"event-type-duplicate-" + type.id}
StartIcon={Icon.FiUpload} StartIcon={FiUpload}
onClick={() => { onClick={() => {
navigator navigator
.share({ .share({
@@ -457,7 +508,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
onClick={() => router.push("/event-types/" + type.id)} onClick={() => router.push("/event-types/" + type.id)}
color="minimal" color="minimal"
className="w-full rounded-none" className="w-full rounded-none"
StartIcon={Icon.FiEdit}> StartIcon={FiEdit}>
{t("edit")} {t("edit")}
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
@@ -467,8 +518,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
color="minimal" color="minimal"
className="w-full rounded-none" className="w-full rounded-none"
data-testid={"event-type-duplicate-" + type.id} data-testid={"event-type-duplicate-" + type.id}
StartIcon={Icon.FiCopy} StartIcon={FiCopy}
onClick={() => openDuplicateModal(type)}> onClick={() => openDuplicateModal(type, group)}>
{t("duplicate")} {t("duplicate")}
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
@@ -480,7 +531,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
setDeleteDialogTypeId(type.id); setDeleteDialogTypeId(type.id);
}} }}
color="destructive" color="destructive"
StartIcon={Icon.FiTrash} StartIcon={FiTrash}
className="w-full rounded-none"> className="w-full rounded-none">
{t("delete")} {t("delete")}
</Button> </Button>
@@ -518,14 +569,13 @@ const EventTypeListHeading = ({
}: EventTypeListHeadingProps): JSX.Element => { }: EventTypeListHeadingProps): JSX.Element => {
return ( return (
<div className="mb-4 flex items-center space-x-2"> <div className="mb-4 flex items-center space-x-2">
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}> <Avatar
<Avatar alt={profile?.name || ""}
alt={profile?.name || ""} href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
imageSrc={`${WEBAPP_URL}/${profile.slug}/avatar.png` || undefined} imageSrc={`${WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
size="sm" size="md"
className="mt-1 inline ltr:mr-2 rtl:ml-2" className="mt-1 inline-flex justify-center"
/> />
</Link>
<div> <div>
<Link <Link
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"} href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
@@ -536,7 +586,7 @@ const EventTypeListHeading = ({
<span className="relative -top-px text-xs text-gray-500 ltr:ml-2 ltr:mr-2 rtl:ml-2"> <span className="relative -top-px text-xs text-gray-500 ltr:ml-2 ltr:mr-2 rtl:ml-2">
<Link href={`/settings/teams/${teamId}/members`}> <Link href={`/settings/teams/${teamId}/members`}>
<Badge variant="gray"> <Badge variant="gray">
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" /> <FiUsers className="mr-1 -mt-px inline h-3 w-3" />
{membershipCount} {membershipCount}
</Badge> </Badge>
</Link> </Link>
@@ -557,7 +607,7 @@ const CreateFirstEventTypeView = () => {
return ( return (
<EmptyScreen <EmptyScreen
Icon={Icon.FiLink} Icon={FiLink}
headline={t("new_event_type_heading")} headline={t("new_event_type_heading")}
description={t("new_event_type_description")} description={t("new_event_type_description")}
/> />
@@ -577,6 +627,8 @@ const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer);
const EventTypesPage = () => { const EventTypesPage = () => {
const { t } = useLocale(); const { t } = useLocale();
const isMobile = useMediaQuery("(max-width: 768px)");
return ( return (
<div> <div>
<Shell heading={t("event_types_page_title")} subtitle={t("event_types_page_subtitle")} CTA={<CTA />}> <Shell heading={t("event_types_page_title")} subtitle={t("event_types_page_subtitle")} CTA={<CTA />}>
@@ -584,26 +636,40 @@ const EventTypesPage = () => {
customLoader={<SkeletonLoader />} customLoader={<SkeletonLoader />}
success={({ data }) => ( success={({ data }) => (
<> <>
{data.eventTypeGroups.map((group, index) => ( {data.eventTypeGroups.length > 1 ? (
<Fragment key={group.profile.slug}> <>
{/* hide list heading when there is only one (current user) */} {isMobile ? (
{(data.eventTypeGroups.length !== 1 || group.teamId) && ( <MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
<EventTypeListHeading ) : (
profile={group.profile} data.eventTypeGroups.map((group, index) => (
membershipCount={group.metadata.membershipCount} <div className="flex flex-col" key={group.profile.slug}>
teamId={group.teamId} <EventTypeListHeading
/> profile={group.profile}
membershipCount={group.metadata.membershipCount}
teamId={group.teamId}
/>
<EventTypeList
types={group.eventTypes}
group={group}
groupIndex={index}
readOnly={group.metadata.readOnly}
/>
</div>
))
)} )}
<EventTypeList </>
types={group.eventTypes} ) : data.eventTypeGroups.length === 1 ? (
group={group} <EventTypeList
groupIndex={index} types={data.eventTypeGroups[0].eventTypes}
readOnly={group.metadata.readOnly} group={data.eventTypeGroups[0]}
/> groupIndex={0}
</Fragment> readOnly={data.eventTypeGroups[0].metadata.readOnly}
))} />
) : (
<CreateFirstEventTypeView />
)}
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView />}
<EmbedDialog /> <EmbedDialog />
</> </>
)} )}
@@ -7,10 +7,9 @@ import { z } from "zod";
import { getSession } from "@calcom/lib/auth"; import { getSession } from "@calcom/lib/auth";
import { APP_NAME } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { User } from "@calcom/prisma/client"; import prisma from "@calcom/prisma";
import { Button, StepCard, Steps } from "@calcom/ui"; import { Button, StepCard, Steps } from "@calcom/ui";
import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars"; import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
@@ -60,7 +59,6 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
`${t("set_availability_getting_started_subtitle_1")}`, `${t("set_availability_getting_started_subtitle_1")}`,
`${t("set_availability_getting_started_subtitle_2")}`, `${t("set_availability_getting_started_subtitle_2")}`,
], ],
skipText: `${t("set_my_availability_later")}`,
}, },
{ {
title: `${t("nearly_there")}`, title: `${t("nearly_there")}`,
-32
View File
@@ -1,32 +0,0 @@
import { GetServerSidePropsContext } from "next";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Meta } from "@calcom/ui";
import { getLayout } from "@components/auth/layouts/AdminLayout";
import { ssrInit } from "@server/lib/ssr";
function AdminUsersView() {
const { t } = useLocale();
return (
<>
<Meta title={t("users")} description={t("users_description")} />
<h1>{t("users_listing")}</h1>
</>
);
}
AdminUsersView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default AdminUsersView;
+3 -4
View File
@@ -7,8 +7,8 @@ import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { Button, Meta } from "@calcom/ui";
import { Button, Icon, Meta } from "@calcom/ui"; import { FiExternalLink } from "@calcom/ui/components/icon";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
@@ -36,7 +36,6 @@ const CtaRow = ({ title, description, className, children }: CtaRowProps) => {
const BillingView = () => { const BillingView = () => {
const { t } = useLocale(); const { t } = useLocale();
const { data: user } = trpc.viewer.me.useQuery();
const [, loadChat] = useChat(); const [, loadChat] = useChat();
const [showChat, setShowChat] = useState(false); const [showChat, setShowChat] = useState(false);
const router = useRouter(); const router = useRouter();
@@ -55,7 +54,7 @@ const BillingView = () => {
<CtaRow <CtaRow
title={t("billing_manage_details_title")} title={t("billing_manage_details_title")}
description={t("billing_manage_details_description")}> description={t("billing_manage_details_description")}>
<Button color="primary" href={billingHref} target="_blank" EndIcon={Icon.FiExternalLink}> <Button color="primary" href={billingHref} target="_blank" EndIcon={FiExternalLink}>
{t("billing_portal")} {t("billing_portal")}
</Button> </Button>
</CtaRow> </CtaRow>
@@ -14,10 +14,10 @@ import {
Dialog, Dialog,
DialogContent, DialogContent,
EmptyScreen, EmptyScreen,
Icon,
Meta, Meta,
AppSkeletonLoader as SkeletonLoader, AppSkeletonLoader as SkeletonLoader,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiLink, FiPlus } from "@calcom/ui/components/icon";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
@@ -35,7 +35,7 @@ const ApiKeysView = () => {
return ( return (
<Button <Button
color="secondary" color="secondary"
StartIcon={Icon.FiPlus} StartIcon={FiPlus}
onClick={() => { onClick={() => {
setApiKeyToEdit(undefined); setApiKeyToEdit(undefined);
setApiKeyModal(true); setApiKeyModal(true);
@@ -75,7 +75,7 @@ const ApiKeysView = () => {
</> </>
) : ( ) : (
<EmptyScreen <EmptyScreen
Icon={Icon.FiLink} Icon={FiLink}
headline={t("create_first_api_key")} headline={t("create_first_api_key")}
description={t("create_first_api_key_description", { appName: APP_NAME })} description={t("create_first_api_key_description", { appName: APP_NAME })}
buttonRaw={<NewApiKeyButton />} buttonRaw={<NewApiKeyButton />}
@@ -1,9 +1,10 @@
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { useSession } from "next-auth/react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import ThemeLabel from "@calcom/features/settings/ThemeLabel";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { APP_NAME } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants";
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { import {
@@ -46,10 +47,10 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
const AppearanceView = () => { const AppearanceView = () => {
const { t } = useLocale(); const { t } = useLocale();
const session = useSession();
const utils = trpc.useContext(); const utils = trpc.useContext();
const { data: user, isLoading } = trpc.viewer.me.useQuery(); const { data: user, isLoading } = trpc.viewer.me.useQuery();
const { data: dataHasTeamPlan, isLoading: isLoadingHasTeamPlan } = trpc.viewer.teams.hasTeamPlan.useQuery();
const { isLoading: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan();
const formMethods = useForm({ const formMethods = useForm({
defaultValues: { defaultValues: {
@@ -74,7 +75,7 @@ const AppearanceView = () => {
}, },
}); });
if (isLoading || isLoadingHasTeamPlan) if (isLoading || isTeamPlanStatusLoading)
return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />; return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
if (!user) return null; if (!user) return null;
@@ -164,7 +165,7 @@ const AppearanceView = () => {
{/* TODO future PR to preview brandColors */} {/* TODO future PR to preview brandColors */}
{/* <Button {/* <Button
color="secondary" color="secondary"
EndIcon={Icon.FiExternalLink} EndIcon={FiExternalLink}
className="mt-6" className="mt-6"
onClick={() => window.open(`${WEBAPP_URL}/${user.username}/${user.eventTypes[0].title}`, "_blank")}> onClick={() => window.open(`${WEBAPP_URL}/${user.username}/${user.eventTypes[0].title}`, "_blank")}>
Preview Preview
@@ -182,18 +183,18 @@ const AppearanceView = () => {
<p className="font-semibold ltr:mr-2 rtl:ml-2"> <p className="font-semibold ltr:mr-2 rtl:ml-2">
{t("disable_cal_branding", { appName: APP_NAME })} {t("disable_cal_branding", { appName: APP_NAME })}
</p> </p>
{!dataHasTeamPlan?.hasTeamPlan && <UpgradeTeamsBadge />} <UpgradeTeamsBadge />
</div> </div>
<p className="mt-0.5 text-gray-600">{t("removes_cal_branding", { appName: APP_NAME })}</p> <p className="mt-0.5 text-gray-600">{t("removes_cal_branding", { appName: APP_NAME })}</p>
</div> </div>
<div className="flex-none"> <div className="flex-none">
<Switch <Switch
id="hideBranding" id="hideBranding"
disabled={!dataHasTeamPlan?.hasTeamPlan} disabled={!hasPaidPlan}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
formMethods.setValue("hideBranding", checked, { shouldDirty: true }) formMethods.setValue("hideBranding", checked, { shouldDirty: true })
} }
checked={!dataHasTeamPlan?.hasTeamPlan ? false : value} checked={hasPaidPlan ? value : false}
/> />
</div> </div>
</div> </div>
@@ -225,36 +226,3 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}; };
export default AppearanceView; export default AppearanceView;
interface ThemeLabelProps {
variant: "light" | "dark" | "system";
value?: "light" | "dark" | null;
label: string;
defaultChecked?: boolean;
register: any;
}
const ThemeLabel = ({ variant, label, value, defaultChecked, register }: ThemeLabelProps) => {
return (
<label
className="relative mb-4 flex-1 cursor-pointer text-center last:mb-0 last:mr-0 sm:mr-4 sm:mb-0"
htmlFor={`theme-${variant}`}>
<input
className="peer absolute top-8 left-8"
type="radio"
value={value}
id={`theme-${variant}`}
defaultChecked={defaultChecked}
{...register("theme")}
/>
<div className="relative z-10 rounded-lg ring-black transition-all peer-checked:ring-2">
<img
aria-hidden="true"
className="cover w-full rounded-lg"
src={`/theme-${variant}.svg`}
alt={`theme ${variant}`}
/>
</div>
<p className="mt-2 text-sm font-medium text-gray-600 peer-checked:text-gray-900">{label}</p>
</label>
);
};
@@ -14,7 +14,6 @@ import {
Badge, Badge,
Button, Button,
EmptyScreen, EmptyScreen,
Icon,
List, List,
ListItem, ListItem,
ListItemText, ListItemText,
@@ -25,6 +24,7 @@ import {
SkeletonText, SkeletonText,
showToast, showToast,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiPlus, FiCalendar } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
@@ -52,7 +52,7 @@ const AddCalendarButton = () => {
return ( return (
<> <>
<Button color="secondary" StartIcon={Icon.FiPlus} href="/apps/categories/calendar"> <Button color="secondary" StartIcon={FiPlus} href="/apps/categories/calendar">
{t("add_calendar")} {t("add_calendar")}
</Button> </Button>
</> </>
@@ -89,7 +89,7 @@ const CalendarsView = () => {
<div> <div>
<div className="mt-4 flex space-x-4 rounded-md border-gray-200 bg-gray-50 p-2 sm:mx-0 sm:p-10 md:border md:p-6 xl:mt-0"> <div className="mt-4 flex space-x-4 rounded-md border-gray-200 bg-gray-50 p-2 sm:mx-0 sm:p-10 md:border md:p-6 xl:mt-0">
<div className=" flex h-9 w-9 items-center justify-center rounded-md border-2 border-gray-200 bg-white p-[6px]"> <div className=" flex h-9 w-9 items-center justify-center rounded-md border-2 border-gray-200 bg-white p-[6px]">
<Icon.FiCalendar className="h-6 w-6" /> <FiCalendar className="h-6 w-6" />
</div> </div>
<div className="flex w-full flex-col space-y-3"> <div className="flex w-full flex-col space-y-3">
@@ -197,7 +197,7 @@ const CalendarsView = () => {
</div> </div>
) : ( ) : (
<EmptyScreen <EmptyScreen
Icon={Icon.FiCalendar} Icon={FiCalendar}
headline={t("no_calendar_installed")} headline={t("no_calendar_installed")}
description={t("no_calendar_installed_description")} description={t("no_calendar_installed_description")}
buttonText={t("add_a_calendar")} buttonText={t("add_a_calendar")}
@@ -15,13 +15,13 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownItem, DropdownItem,
DropdownMenuTrigger, DropdownMenuTrigger,
Icon,
List, List,
Meta, Meta,
showToast, showToast,
SkeletonContainer, SkeletonContainer,
SkeletonText, SkeletonText,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiAlertCircle, FiMoreHorizontal, FiTrash } from "@calcom/ui/components/icon";
import AppListCard from "@components/AppListCard"; import AppListCard from "@components/AppListCard";
@@ -85,7 +85,7 @@ const ConferencingLayout = () => {
<div> <div>
<Dropdown> <Dropdown>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button StartIcon={Icon.FiMoreHorizontal} variant="icon" color="secondary" /> <Button StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem> <DropdownMenuItem>
@@ -93,7 +93,7 @@ const ConferencingLayout = () => {
type="button" type="button"
color="destructive" color="destructive"
disabled={app.isGlobal} disabled={app.isGlobal}
StartIcon={Icon.FiTrash} StartIcon={FiTrash}
onClick={() => { onClick={() => {
setDeleteCredentialId(app.credentialIds[0]); setDeleteCredentialId(app.credentialIds[0]);
setDeleteAppModal(true); setDeleteAppModal(true);
@@ -114,7 +114,7 @@ const ConferencingLayout = () => {
title={t("Remove app")} title={t("Remove app")}
description={t("are_you_sure_you_want_to_remove_this_app")} description={t("are_you_sure_you_want_to_remove_this_app")}
type="confirmation" type="confirmation"
Icon={Icon.FiAlertCircle}> Icon={FiAlertCircle}>
<DialogFooter> <DialogFooter>
<Button color="primary" onClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}> <Button color="primary" onClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}>
{t("yes_remove_app")} {t("yes_remove_app")}
@@ -5,6 +5,7 @@ import { Controller, useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { nameOfDay } from "@calcom/lib/weekday";
import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { import {
Button, Button,
@@ -20,7 +21,6 @@ import {
} from "@calcom/ui"; } from "@calcom/ui";
import { withQuery } from "@lib/QueryCell"; import { withQuery } from "@lib/QueryCell";
import { nameOfDay } from "@lib/core/i18n/weekday";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
+19 -8
View File
@@ -1,5 +1,6 @@
import { IdentityProvider } from "@prisma/client"; import { IdentityProvider } from "@prisma/client";
import crypto from "crypto"; import crypto from "crypto";
import MarkdownIt from "markdown-it";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { signOut } from "next-auth/react"; import { signOut } from "next-auth/react";
import { BaseSyntheticEvent, useRef, useState } from "react"; import { BaseSyntheticEvent, useRef, useState } from "react";
@@ -9,6 +10,7 @@ import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { ErrorCode } from "@calcom/lib/auth"; import { ErrorCode } from "@calcom/lib/auth";
import { APP_NAME } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import turndown from "@calcom/lib/turndownService";
import { TRPCClientErrorLike } from "@calcom/trpc/client"; import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { AppRouter } from "@calcom/trpc/server/routers/_app"; import { AppRouter } from "@calcom/trpc/server/routers/_app";
@@ -22,7 +24,6 @@ import {
DialogFooter, DialogFooter,
DialogTrigger, DialogTrigger,
Form, Form,
Icon,
ImageUploader, ImageUploader,
Label, Label,
Meta, Meta,
@@ -33,21 +34,25 @@ import {
SkeletonContainer, SkeletonContainer,
SkeletonText, SkeletonText,
TextField, TextField,
Editor,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiAlertTriangle, FiTrash2 } from "@calcom/ui/components/icon";
import TwoFactor from "@components/auth/TwoFactor"; import TwoFactor from "@components/auth/TwoFactor";
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability"; import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
const md = new MarkdownIt("default", { html: true, breaks: true });
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => { const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return ( return (
<SkeletonContainer> <SkeletonContainer>
<Meta title={title} description={description} /> <Meta title={title} description={description} />
<div className="mt-6 mb-8 space-y-6 divide-y"> <div className="mt-6 mb-8 space-y-6 divide-y">
<div className="flex items-center"> <div className="flex items-center">
<SkeletonAvatar className=" h-12 w-12 px-4" /> <SkeletonAvatar className="h-12 w-12 px-4" />
<SkeletonButton className=" h-6 w-32 rounded-md p-5" /> <SkeletonButton className="h-6 w-32 rounded-md p-5" />
</div> </div>
<SkeletonText className="h-8 w-full" /> <SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" /> <SkeletonText className="h-8 w-full" />
@@ -229,7 +234,7 @@ const ProfileView = () => {
{/* Delete account Dialog */} {/* Delete account Dialog */}
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}> <Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button data-testid="delete-account" color="destructive" className="mt-1" StartIcon={Icon.FiTrash2}> <Button data-testid="delete-account" color="destructive" className="mt-1" StartIcon={FiTrash2}>
{t("delete_account")} {t("delete_account")}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
@@ -237,7 +242,7 @@ const ProfileView = () => {
title={t("delete_account_modal_title")} title={t("delete_account_modal_title")}
description={t("confirm_delete_account_modal", { appName: APP_NAME })} description={t("confirm_delete_account_modal", { appName: APP_NAME })}
type="creation" type="creation"
Icon={Icon.FiAlertTriangle}> Icon={FiAlertTriangle}>
<> <>
<p className="mb-7">{t("delete_account_confirmation_message", { appName: APP_NAME })}</p> <p className="mb-7">{t("delete_account_confirmation_message", { appName: APP_NAME })}</p>
{isCALIdentityProviver && ( {isCALIdentityProviver && (
@@ -278,7 +283,7 @@ const ProfileView = () => {
title={t("confirm_password")} title={t("confirm_password")}
description={t("confirm_password_change_email")} description={t("confirm_password_change_email")}
type="creation" type="creation"
Icon={Icon.FiAlertTriangle}> Icon={FiAlertTriangle}>
<> <>
<PasswordField <PasswordField
data-testid="password" data-testid="password"
@@ -361,9 +366,15 @@ const ProfileForm = ({
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} /> <TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
</div> </div>
<div className="mt-8"> <div className="mt-8">
<TextField label={t("about")} hint={t("bio_hint")} {...formMethods.register("bio")} /> <Label>{t("about")}</Label>
<Editor
getText={() => md.render(formMethods.getValues("bio") || "")}
setText={(value: string) => {
formMethods.setValue("bio", turndown(value), { shouldDirty: true });
}}
excludedToolbarItems={["blockType"]}
/>
</div> </div>
<Button disabled={isDisabled} color="primary" className="mt-8" type="submit"> <Button disabled={isDisabled} color="primary" className="mt-8" type="submit">
{t("update")} {t("update")}
</Button> </Button>
+96 -16
View File
@@ -1,51 +1,96 @@
import { IdentityProvider } from "@prisma/client"; import { IdentityProvider } from "@prisma/client";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { identityProviderNameMap } from "@calcom/lib/auth"; import { identityProviderNameMap } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { userMetadata } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, Form, Meta, PasswordField, showToast } from "@calcom/ui"; import { Button, Form, Meta, PasswordField, Select, SettingsToggle, showToast } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
type ChangePasswordFormValues = { type ChangePasswordSessionFormValues = {
oldPassword: string; oldPassword: string;
newPassword: string; newPassword: string;
sessionTimeout?: number;
}; };
const PasswordView = () => { const PasswordView = () => {
const { t } = useLocale(); const { t } = useLocale();
const utils = trpc.useContext();
const { data: user } = trpc.viewer.me.useQuery(); const { data: user } = trpc.viewer.me.useQuery();
const metadata = userMetadata.parse(user?.metadata);
const mutation = trpc.viewer.auth.changePassword.useMutation({ const sessionMutation = trpc.viewer.updateProfile.useMutation({
onSuccess: () => {
showToast(t("session_timeout_changed"), "success");
formMethods.reset(formMethods.getValues());
},
onSettled: () => {
utils.viewer.me.invalidate();
},
onMutate: async ({ metadata }) => {
await utils.viewer.me.cancel();
const previousValue = utils.viewer.me.getData();
const previousMetadata = userMetadata.parse(previousValue?.metadata);
if (previousValue && metadata?.sessionTimeout) {
utils.viewer.me.setData(undefined, {
...previousValue,
metadata: { ...previousMetadata, sessionTimeout: metadata?.sessionTimeout },
});
}
return { previousValue };
},
onError: (error, _, context) => {
if (context?.previousValue) {
utils.viewer.me.setData(undefined, context.previousValue);
}
showToast(`${t("session_timeout_change_error")}, ${error.message}`, "error");
},
});
const passwordMutation = trpc.viewer.auth.changePassword.useMutation({
onSuccess: () => { onSuccess: () => {
showToast(t("password_has_been_changed"), "success"); showToast(t("password_has_been_changed"), "success");
formMethods.resetField("oldPassword");
formMethods.resetField("newPassword");
}, },
onError: (error) => { onError: (error) => {
showToast(`${t("error_updating_password")}, ${error.message}`, "error"); showToast(`${t("error_updating_password")}, ${error.message}`, "error");
}, },
}); });
const formMethods = useForm<ChangePasswordFormValues>({ const formMethods = useForm<ChangePasswordSessionFormValues>({
defaultValues: { defaultValues: {
oldPassword: "", oldPassword: "",
newPassword: "", newPassword: "",
sessionTimeout: metadata?.sessionTimeout,
}, },
}); });
const { const sessionTimeoutWatch = formMethods.watch("sessionTimeout");
register,
formState: { isSubmitting },
} = formMethods;
const handleSubmit = (values: ChangePasswordFormValues) => { const handleSubmit = (values: ChangePasswordSessionFormValues) => {
const { oldPassword, newPassword } = values; const { oldPassword, newPassword, sessionTimeout } = values;
mutation.mutate({ oldPassword, newPassword }); if (oldPassword && newPassword) {
passwordMutation.mutate({ oldPassword, newPassword });
}
if (metadata?.sessionTimeout !== sessionTimeout) {
sessionMutation.mutate({ metadata: { ...metadata, sessionTimeout } });
}
}; };
const timeoutOptions = [5, 10, 15].map((mins) => ({
label: t("multiple_duration_mins", { count: mins }),
value: mins,
}));
const isDisabled = formMethods.formState.isSubmitting || !formMethods.formState.isDirty;
return ( return (
<> <>
<Meta title={t("password")} description={t("password_description")} /> <Meta title={t("password")} description={t("password_description")} />
@@ -68,24 +113,59 @@ const PasswordView = () => {
<Form form={formMethods} handleSubmit={handleSubmit}> <Form form={formMethods} handleSubmit={handleSubmit}>
<div className="max-w-[38rem] sm:flex sm:space-x-4"> <div className="max-w-[38rem] sm:flex sm:space-x-4">
<div className="flex-grow"> <div className="flex-grow">
<PasswordField {...register("oldPassword")} label={t("old_password")} /> <PasswordField {...formMethods.register("oldPassword")} label={t("old_password")} />
</div> </div>
<div className="flex-grow"> <div className="flex-grow">
<PasswordField {...register("newPassword")} label={t("new_password")} /> <PasswordField {...formMethods.register("newPassword")} label={t("new_password")} />
</div> </div>
</div> </div>
<p className="text-sm text-gray-600"> <p className="mt-4 max-w-[38rem] text-sm text-gray-600">
<Trans i18nKey="invalid_password_hint"> <Trans i18nKey="invalid_password_hint">
Password must be at least at least 7 characters, mix of uppercase & lowercase letters, and Password must be at least at least 7 characters, mix of uppercase & lowercase letters, and
contain at least 1 number contain at least 1 number.
</Trans> </Trans>
</p> </p>
<div className="mt-8 border-t border-gray-200 py-8">
<SettingsToggle
title={t("session_timeout")}
description={t("session_timeout_description")}
checked={sessionTimeoutWatch !== undefined}
data-testid="session-check"
onCheckedChange={(e) => {
if (!e) {
formMethods.setValue("sessionTimeout", undefined, { shouldDirty: true });
} else {
formMethods.setValue("sessionTimeout", 10, { shouldDirty: true });
}
}}
/>
{sessionTimeoutWatch && (
<div className="mt-4 text-sm">
<div className="flex items-center">
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">{t("session_timeout_after")}</p>
<Select
options={timeoutOptions}
defaultValue={
metadata?.sessionTimeout
? timeoutOptions.find((tmo) => tmo.value === metadata.sessionTimeout)
: timeoutOptions[1]
}
isSearchable={false}
className="block h-[36px] !w-auto min-w-0 flex-none rounded-md text-sm"
onChange={(event) => {
formMethods.setValue("sessionTimeout", event?.value, { shouldDirty: true });
}}
/>
</div>
</div>
)}
</div>
{/* TODO: Why is this Form not submitting? Hacky fix but works */} {/* TODO: Why is this Form not submitting? Hacky fix but works */}
<Button <Button
color="primary" color="primary"
className="mt-8" className="mt-8"
type="submit" type="submit"
disabled={isSubmitting || mutation.isLoading}> disabled={isDisabled || passwordMutation.isLoading || sessionMutation.isLoading}>
{t("update")} {t("update")}
</Button> </Button>
</Form> </Form>
+7 -6
View File
@@ -5,15 +5,16 @@ import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired"; import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps"; import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Alert, Button, EmailField, PasswordField, TextField, HeadSeo } from "@calcom/ui"; import { Alert, Button, EmailField, HeadSeo, PasswordField, TextField } from "@calcom/ui";
import { asStringOrNull } from "@calcom/web/lib/asStringOrNull";
import { WEBAPP_URL } from "@calcom/web/lib/config/constants"; import { asStringOrNull } from "../lib/asStringOrNull";
import prisma from "@calcom/web/lib/prisma"; import { IS_GOOGLE_LOGIN_ENABLED } from "../server/lib/constants";
import { IS_GOOGLE_LOGIN_ENABLED } from "@calcom/web/server/lib/constants"; import { ssrInit } from "../server/lib/ssr";
import { ssrInit } from "@calcom/web/server/lib/ssr";
type FormValues = { type FormValues = {
username: string; username: string;
+23 -6
View File
@@ -1,4 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import MarkdownIt from "markdown-it";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -12,16 +13,21 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams"; import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { Avatar, Button, Icon, HeadSeo, AvatarGroup } from "@calcom/ui"; import { Avatar, Button, HeadSeo, AvatarGroup } from "@calcom/ui";
import { FiArrowRight } from "@calcom/ui/components/icon";
import { useToggleQuery } from "@lib/hooks/useToggleQuery"; import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import Team from "@components/team/screens/Team"; import Team from "@components/team/screens/Team";
import { ssrInit } from "@server/lib/ssr";
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>; export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
function TeamPage({ team }: TeamPageProps) { function TeamPage({ team }: TeamPageProps) {
useTheme(); useTheme(team.theme);
const showMembers = useToggleQuery("members"); const showMembers = useToggleQuery("members");
const { t } = useLocale(); const { t } = useLocale();
const isEmbed = useIsEmbed(); const isEmbed = useIsEmbed();
@@ -36,12 +42,12 @@ function TeamPage({ team }: TeamPageProps) {
}, [telemetry, router.asPath]); }, [telemetry, router.asPath]);
const EventTypes = () => ( const EventTypes = () => (
<ul className="rounded-md border border-gray-200 dark:border-gray-700"> <ul className="dark:border-darkgray-300 rounded-md border border-gray-200">
{team.eventTypes.map((type, index) => ( {team.eventTypes.map((type, index) => (
<li <li
key={index} key={index}
className={classNames( className={classNames(
"dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600", "dark:bg-darkgray-100 dark:border-darkgray-300 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50",
!isEmbed && "bg-white" !isEmbed && "bg-white"
)}> )}>
<Link <Link
@@ -74,6 +80,8 @@ function TeamPage({ team }: TeamPageProps) {
const teamName = team.name || "Nameless Team"; const teamName = team.name || "Nameless Team";
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
return ( return (
<div> <div>
<HeadSeo <HeadSeo
@@ -90,7 +98,14 @@ function TeamPage({ team }: TeamPageProps) {
<p className="font-cal dark:text-darkgray-900 mb-2 text-2xl tracking-wider text-gray-900"> <p className="font-cal dark:text-darkgray-900 mb-2 text-2xl tracking-wider text-gray-900">
{teamName} {teamName}
</p> </p>
<p className="dark:text-darkgray-500 mt-2 text-sm font-normal text-gray-500">{team.bio}</p> {!isBioEmpty && (
<>
<div
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(team.bio || "") }}
/>
</>
)}
</div> </div>
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />} {(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
{!showMembers.isOn && team.eventTypes.length > 0 && ( {!showMembers.isOn && team.eventTypes.length > 0 && (
@@ -113,7 +128,7 @@ function TeamPage({ team }: TeamPageProps) {
<aside className="mt-8 flex justify-center text-center dark:text-white"> <aside className="mt-8 flex justify-center text-center dark:text-white">
<Button <Button
color="minimal" color="minimal"
EndIcon={Icon.FiArrowRight} EndIcon={FiArrowRight}
className="dark:hover:bg-darkgray-200" className="dark:hover:bg-darkgray-200"
href={`/team/${team.slug}?members=1`} href={`/team/${team.slug}?members=1`}
shallow={true}> shallow={true}>
@@ -130,6 +145,7 @@ function TeamPage({ team }: TeamPageProps) {
} }
export const getServerSideProps = async (context: GetServerSidePropsContext) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug; const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
const team = await getTeamWithMembers(undefined, slug); const team = await getTeamWithMembers(undefined, slug);
@@ -147,6 +163,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return { return {
props: { props: {
team, team,
trpcState: ssr.dehydrate(),
}, },
}; };
}; };
+8 -4
View File
@@ -2,11 +2,11 @@ import { GetServerSidePropsContext } from "next";
import { privacyFilteredLocations, LocationObject } from "@calcom/core/location"; import { privacyFilteredLocations, LocationObject } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import { getWorkingHours } from "@calcom/lib/availability";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull } from "@lib/asStringOrNull"; import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability";
import getBooking, { GetBookingType } from "@lib/getBooking"; import getBooking, { GetBookingType } from "@lib/getBooking";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr"; import { EmbedProps } from "@lib/withEmbedSsr";
@@ -43,6 +43,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
slug: true, slug: true,
logo: true, logo: true,
hideBranding: true, hideBranding: true,
brandColor: true,
darkBrandColor: true,
theme: true,
eventTypes: { eventTypes: {
where: { where: {
slug: typeParam, slug: typeParam,
@@ -68,6 +71,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}, },
}, },
}, },
title: true, title: true,
availability: true, availability: true,
description: true, description: true,
@@ -150,10 +154,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
name: team.name || team.slug, name: team.name || team.slug,
slug: team.slug, slug: team.slug,
image: team.logo, image: team.logo,
theme: null as string | null, theme: team.theme,
weekStart: "Sunday", weekStart: "Sunday",
brandColor: "" /* TODO: Add a way to set a brand color for Teams */, brandColor: team.brandColor,
darkBrandColor: "" /* TODO: Add a way to set a brand color for Teams */, darkBrandColor: team.darkBrandColor,
}, },
date: dateParam, date: dateParam,
eventType: eventTypeObject, eventType: eventTypeObject,
+3 -4
View File
@@ -1,5 +1,4 @@
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations"; import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
@@ -68,6 +67,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
slug: true, slug: true,
name: true, name: true,
logo: true, logo: true,
theme: true,
brandColor: true,
darkBrandColor: true,
}, },
}, },
users: { users: {
@@ -129,9 +131,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
// FIXME: This slug is used as username on success page which is wrong. This is correctly set as username for user booking. // FIXME: This slug is used as username on success page which is wrong. This is correctly set as username for user booking.
slug: "team/" + eventTypeObject.slug, slug: "team/" + eventTypeObject.slug,
image: eventTypeObject.team?.logo || null, image: eventTypeObject.team?.logo || null,
theme: null as string | null /* Teams don't have a theme, and `BookingPage` uses it */,
brandColor: null /* Teams don't have a brandColor, and `BookingPage` uses it */,
darkBrandColor: null /* Teams don't have a darkBrandColor, and `BookingPage` uses it */,
eventName: null, eventName: null,
}, },
eventType: eventTypeObject, eventType: eventTypeObject,
+3 -2
View File
@@ -4,7 +4,8 @@ import { TeamsListing } from "@calcom/features/ee/teams/components";
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon } from "@calcom/ui"; import { Button } from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
@@ -17,7 +18,7 @@ function Teams() {
CTA={ CTA={
<Button <Button
variant="fab" variant="fab"
StartIcon={Icon.FiPlus} StartIcon={FiPlus}
type="button" type="button"
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}> href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
{t("new")} {t("new")}
+22 -12
View File
@@ -1,5 +1,5 @@
import DailyIframe from "@daily-co/daily-js"; import DailyIframe from "@daily-co/daily-js";
import { NextPageContext } from "next"; import { GetServerSidePropsContext } from "next";
import { getSession } from "next-auth/react"; import { getSession } from "next-auth/react";
import Head from "next/head"; import Head from "next/head";
import { useEffect } from "react"; import { useEffect } from "react";
@@ -9,10 +9,13 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps"; import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { ssrInit } from "@server/lib/ssr";
export type JoinCallPageProps = inferSSRProps<typeof getServerSideProps>; export type JoinCallPageProps = inferSSRProps<typeof getServerSideProps>;
export default function JoinCall(props: JoinCallPageProps) { export default function JoinCall(props: JoinCallPageProps) {
const { t } = useLocale(); const { t } = useLocale();
const { meetingUrl, meetingPassword } = props;
useEffect(() => { useEffect(() => {
const callFrame = DailyIframe.createFrame({ const callFrame = DailyIframe.createFrame({
@@ -36,19 +39,20 @@ export default function JoinCall(props: JoinCallPageProps) {
width: "100%", width: "100%",
height: "100%", height: "100%",
}, },
url: meetingUrl,
...(typeof meetingPassword === "string" && { token: meetingPassword }),
}); });
callFrame.join({ callFrame.join();
url: props.booking.references[0].meetingUrl ?? "", return () => {
showLeaveButton: true, callFrame.destroy();
...(props.booking.references[0].meetingPassword };
? { token: props.booking.references[0].meetingPassword } }, []);
: null),
}); const title = `${APP_NAME} Video`;
}, [props.booking?.references]);
return ( return (
<> <>
<Head> <Head>
<title>{APP_NAME} Video</title> <title>{title}</title>
<meta name="title" content={APP_NAME + " Video"} /> <meta name="title" content={APP_NAME + " Video"} />
<meta name="description" content={t("quick_video_meeting")} /> <meta name="description" content={t("quick_video_meeting")} />
<meta property="og:image" content={SEO_IMG_OGIMG_VIDEO} /> <meta property="og:image" content={SEO_IMG_OGIMG_VIDEO} />
@@ -77,7 +81,9 @@ export default function JoinCall(props: JoinCallPageProps) {
); );
} }
export async function getServerSideProps(context: NextPageContext) { export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const booking = await prisma.booking.findUnique({ const booking = await prisma.booking.findUnique({
where: { where: {
uid: context.query.uid as string, uid: context.query.uid as string,
@@ -144,7 +150,11 @@ export async function getServerSideProps(context: NextPageContext) {
return { return {
props: { props: {
booking: bookingObj, meetingUrl: bookingObj.references[0].meetingUrl ?? "",
...(typeof bookingObj.references[0].meetingPassword === "string" && {
meetingPassword: bookingObj.references[0].meetingPassword,
}),
trpcState: ssr.dehydrate(),
}, },
}; };
} }
Loaded 100 of 349 files, more files were not shown because too many files have changed in this diff. Show more