Compare commits

...
147 Commits
Author SHA1 Message Date
Peer Richelsen 791927e45e Update package.json
release.yml / release (push) Canceled after 0s
2023-01-20 23:06:43 +01:00
Alex van Andel fa984e1568 Changed a neutral- classes to gray (#6603)
* Changed a neutral- classes to gray

* Changed all border-1 to border
2023-01-20 23:04:58 +01:00
Peer Richelsen 9b197da5be Update README.md
release.yml / release (push) Canceled after 0s
2023-01-20 21:58:11 +01:00
Peer Richelsen b7c316b01b Update README.md 2023-01-20 21:57:52 +01:00
Udit Takkar 5ab56310bf fix: add delete in dropdown (#6599)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-20 14:57:14 -05:00
Udit Takkar 52e318ca18 fix: show button on empty string (#6601)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-20 12:27:58 -07:00
Omar López 8d078564dd Bye bye submodules (#6585)
* WIP

* Uses ssh instead

* Update .gitignore

* Update .gitignore

* Update Makefile

* Update git-setup.sh

* Update git-setup.sh

* Replaced Makefile with bash script

* Update package.json
2023-01-20 12:17:51 -07:00
7b5482e83d [CAL-808] /availability/single - UI issue on buttons beside time inputs (#6561)
* [CAL-808] /availability/single - UI issue on buttons beside time inputs

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

* Update packages/features/schedules/components/Schedule.tsx

* create new translation for tooltip

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-20 17:56:28 +00:00
sean-brydon 23086476b3 Tailwind Merge (#6596)
* Tailwind Merge

* Fix merge classNames
2023-01-20 15:19:12 +01:00
Nafees Nazik 00cab5c41f fix: react-select overflow issue in some modals. (#6587)
* feat: add a disable overflow prop

* feat: use the disable overflow prop
2023-01-20 13:29:37 +00:00
sean-brydon fbc27e1d52 Fix booking filters not working for admin (#6576) 2023-01-19 19:23:58 +01:00
Nafees Nazik e1c9a8cbfc fix: i18n placeholder in awaiting approval email (#6573) 2023-01-19 16:56:44 +01:00
Nafees Nazik 8ee109e975 fix: app list item ui issues in /settings/my-account/conferencing (#6565)
* fix: border color

* fix: padding and title line-height

* feat: add app-list card

* feat: use the AppListCard component

* nit: remove unused imports
2023-01-19 15:02:01 +00:00
sean-brydonandAlex van Andel cfaecc3e02 Icon small button variant (#6525)
* Icon small button variant

* Rename

* Update CVA + Update button WIP

* add ticket ref and fix padding

* Update button and all uses of Icon button

* SB deployment test

* Fixing type error

* Fix lint

* Add icon button to canvas

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-01-19 14:55:32 +00:00
Syed Ali Shahbaz 18246e0dc1 path.join causing issues on windows so we keep path like the other exports (#6568) 2023-01-19 13:21:51 +00:00
sean-brydon 6dcb16724c Fixing tabs again (#6566) 2023-01-19 12:05:00 +00:00
d4c5a906b5 App Store Templates (#5289)
* Start by moving what we have to _templates

* WIP

* WIP

* Add create/edit/delete template commands

* Type fixes cli

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-01-18 15:30:25 -07:00
Alex van Andel 27e8d73b82 Revert "experiment: disco border (#6531)"
This reverts commit b5f274d663.
2023-01-18 19:29:36 +00:00
Alex van Andel 25eae4a546 find indexes instead of just one (#6547) 2023-01-18 12:26:07 -07:00
Udit Takkar f663774fee feat: add most popular section in apps (#6539)
* feat: add most popular section in apps

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

* chore: remove margin

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

* fix: merge conflict

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

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-18 11:48:10 -07:00
Nafees NazikandLeo Giovanetti e2258fba27 Feat: webhook form UX improvement (#6448)
* feat: add type and required attribute

* feat: disable ping button when subscriberUrl is
not present

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-01-18 16:02:20 +00:00
Carina WollendorferandCarinaWolli d0d8878f34 Adds ability to add custom sender names for workflow emails (#6463)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-18 15:32:39 +01:00
Udit Takkar 59ff6c35ee fix: limits tab in event types (#6545)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-18 12:58:13 +01:00
Alex van Andel 3c07e305ce Create local next.config.js, add storybook-static to .gitignore (#6544) 2023-01-18 11:54:03 +00:00
Alex van Andel b5f274d663 experiment: disco border (#6531)
* Had a play, looking good (now needs isLoading state or however we want to do that)

* Also add tailwind config..

* Don't show conic bg on lightmode

* Slow down animation, show on light mode

* Apply disco border both on light/dark mode & only on success page

* 5s->8s animation speed, remove border crisping up the corners
2023-01-18 10:40:06 +00:00
Peer Richelsen 6c3ae984fc add tracking URL to LicenseRequired (#6526) 2023-01-17 21:49:40 +00:00
Mohammed Cherfaoui a2e9bf0504 fix toast dimensions issues (#6522) 2023-01-18 02:45:04 +05:30
GitStart-Cal.comandgitstart-calcom 8aa089efe6 [CAL-760] Emails: Design Tokens are not update to 2.0 (#6530)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-01-17 22:14:16 +01:00
sean-brydonandzomars de64d3d9d4 Move exports up a folder (#6519)
* Move exports up a folder

* Fix imports

* Fix imports again

Co-authored-by: zomars <zomars@me.com>
2023-01-17 12:10:47 -07:00
Udit Takkar 71833c0737 fix: type error and border (#6520)
* fix: type error and border

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

* fix: sepeator

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

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-17 17:12:22 +00:00
Udit Takkar c78f4deaf7 fix: use suffix in text field (#6532)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-17 18:08:00 +01:00
Hariom Balhara 24fc7d611f Add routing-forms README (#6506) 2023-01-17 17:58:10 +01:00
sean-brydonandHariom Balhara 8afbc3b457 Fixing horizontal tabs (#6454)
* Fix spacing and height

* Testing if its timing out or something else

* So, timep=out is the problem. Increase the timeout to 1s for now

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-01-17 16:36:06 +00:00
sean-brydon da18aae21c Adding rtl support for SB (#6523) 2023-01-17 13:38:14 +01:00
Nafees Nazik 87a0d533ef fix: german translation (#6512) 2023-01-17 13:35:35 +01:00
Nafees Nazik 7cdd453953 fix: button shifting while focusing (#6521)
* fix: remove border on focus

* refactor: remove dark mode styles
2023-01-17 12:21:05 +00:00
Omar López 8143816c18 v2.5.0
release.yml / release (push) Canceled after 0s
2023-01-16 15:34:47 -07:00
Nafees Nazik 8243ac3c00 Fix: modal content hidden (#6508) 2023-01-16 18:08:34 +01:00
Alex van Andel 207e120d66 Bring back PublicShell (#6505) 2023-01-16 14:59:54 +01:00
Ronit PandaandPeer Richelsen b25d04da1c 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>
2023-01-16 12:57:38 +00:00
Alex van Andel 7064acf756 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
2023-01-16 12:18:18 +00:00
Alex van Andel 716407ad90 Added fallback to hostsFixed too which is used in COLLECTIVE (#6500) 2023-01-16 10:46:06 +00:00
GitStart-Cal.comandgitstart-calcom af0b08bb2b [CAL-677] /event-type : Event Setup sidebar item doesn't reflect multiple durations (#6390)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-01-16 11:45:54 +01:00
Hariom Balhara 3e27735ccb Disable redirect for cancellation page (#6498) 2023-01-16 09:20:50 +00:00
Nafees Nazik 76c087f29c 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
2023-01-16 08:48:07 +00:00
Peer RichelsenandAlex van Andel e05bcf98c2 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>
2023-01-15 18:54:52 +01:00
Udit TakkarandPeer Richelsen 636e88d467 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>
2023-01-15 11:40:40 +00:00
Nafees Nazik 34a61214d3 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
2023-01-14 20:11:28 +01:00
Nafees Nazik b1af30e026 fix: german translation for reschedule booking email subject (#6483)
* fix: german translation for reschedule subject

* fix: add a full stop
2023-01-14 19:10:30 +01:00
Nafees Nazik 870b59d617 fix: darkmode in booking/[id] page (#6458)
* fix: text-area style

* fix: border styles
2023-01-14 16:49:15 +01:00
Peer Richelsen e25094ccab i18n udpate 2023-01-14 02:12:07 +01:00
Carina WollendorferandCarinaWolli 1376f0991f Reusable Upgrade component for teams only features (#6473)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-14 01:48:19 +01:00
443329b99f 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>
2023-01-13 17:42:13 -07:00
Joe Au-Yeung a1fc4ec26d Disable email input if new user invited to team (#6471) 2023-01-13 17:21:57 -07:00
Udit TakkarandPeer Richelsen d462298e79 fix: remove website url in download recording (#6466)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-14 00:03:13 +00:00
Nafees NazikandPeer Richelsen 965cff38c2 fix: HOSTED_CAL_FEATURES env doesn't get passed to the frontend (#6392)
* fix: rename HOSTED_CAL_FEATURES into NEXT_PUBLIC_HOSTED_CAL_FEATURES

* chore: add the new env to example

* fix: docs link

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-13 23:51:37 +00:00
Joe Au-Yeungandkodiakhq[bot] f96e909dce Team invites - transform email or username to lower case (#6470)
* Fix string

* Use current hashPassword

* Transform email to lowercase

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-13 18:58:41 +00:00
Leo Giovanetti a4d6f7da62 Fixing GCal emailing notif and sequential deleting (#6460) 2023-01-13 11:29:42 -07:00
Nafees Nazikandkodiakhq[bot] ce9413e0dd fix: missing empty state in /apps/installed/web3 (#6377)
* refactor: turn enum into const assertion

* feat: add web3 to icon list

* fix: type

* fix: type cast query params and add category list

* fix: conditional rendering

* feat: add i18n keys

* fix: type errors

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-13 14:12:43 +00:00
Peer Richelsen 8fc0dfb763 booking page: only scroll to bottom on mobile (#6453)
* move useMediaQuery into packages

* revert logo.tsx
2023-01-13 17:40:02 +05:30
690edcf0a7 New Crowdin translations by Github Action (#6444)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-12 23:21:06 +00:00
Omar López b7dbf10068 v2.4.7
release.yml / release (push) Canceled after 0s
2023-01-12 16:10:07 -07:00
zomars 83b4ba94fc testing auto crowdin 2023-01-12 16:01:40 -07:00
62a09e7098 Small improvements for constants (#6409)
* use IS_SELF_HOSTED for CalComVersion instead of an additional check for hosted and self-hosted versions

* use IS_SELF_HOSTED for HOSTED_CAL_FEATURES constant to prevent an additional check

Co-authored-by: René Müller <rene.mueller@clicksports.de>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-12 22:25:25 +00:00
d2c4e2894d Check for session in addition for isPublic prop (#6443)
* Check for session in addition for isPublic prop

* Cleaner, more readable version

* Less code same result, no rerender

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-01-12 22:11:15 +00:00
Joe Au-YeungandOmar López 070d314897 Increase error logging details (#6442)
Co-authored-by: Omar López <zomars@me.com>
2023-01-12 21:43:40 +00:00
zomars 0a76368e98 Run required checks from Crowin PRs 2023-01-12 14:22:11 -07:00
99287d9eb4 Feature/fixed hosts (#6423)
* Save design updates for fixed round robin support

* wip - added Host relation

* DRY hostsFixed select

* Changes to allow isFixed in the Availability page

* Allow booking with fixed hosts

* Replace users with hosts if hosts is set

* Also prefer hosts over users here

* Prevent duplicates when hosts is saved

* Accidental slot duplication

* Attempt at making isFixed optional

* Sydney and Shiraz can live in harmony again

* No fixed hosts causes every to be true..

* Make hosts undefinable

* Small handleNewBooking fixes

* Similar fix to the hosts to check for empty-ness

* Default to isFixed false instead of true

* Fix event type list avatars

* Filter availableTimeSlots, collective ts's wont magically re-enable.

* (Further) Fixes to getAggregateWorkingHours

* Weird userId artifact that preceeds this branch, investigate later

* On user delete, remove host, on event type delete, also remove host

* Dynamic event types were incorrectly marked as isFixed=false

* Fixed notFound error when there are no users (but hosts)

* Fixes userIsOwner

* Oops, fixed isFixed users being included correctly

* Fixed Button styling on secondary darkmode

* Create exclusivity in selectable options

* fix: Location dropdown is overflowing #6376 (#6415)

* add `menuPlacement` react-select's props to `getReactSelectProps` to avoid dropdown overflow on small screen.
By default, set to "auto".

* CALCOM-6362 - [CAL-728] App Sidebar. Child items aren't sized/spaced properly (#6424)

* [CAL-728] App Sidebar. Child items aren't sized/spaced properly

* fix: undo logo size

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: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Fixing colors (#6429)

* Update website

* Update console

* Update yarn.lock

* Uses disable instead of filtering to be more clear

* Update EventTeamTab.tsx

* Merge conflict cleanup

* During test cases the dayjs() utcOffset is local time, this fixes that

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: Thomas Brodusch <3238312+thomasbrodusch@users.noreply.github.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: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2023-01-12 14:09:12 -07:00
zomars 100035cb89 Update yarn.lock 2023-01-12 11:37:19 -07:00
zomars 1750dbead6 Update console 2023-01-12 11:37:11 -07:00
zomars 4c8ed3d11f Update website 2023-01-12 11:36:54 -07:00
sean-brydon f21c4eb11e Fixing colors (#6429) 2023-01-12 16:57:43 +00:00
3f71a41d7e CALCOM-6362 - [CAL-728] App Sidebar. Child items aren't sized/spaced properly (#6424)
* [CAL-728] App Sidebar. Child items aren't sized/spaced properly

* fix: undo logo size

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: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-12 16:44:41 +00:00
Thomas Brodusch 1ba191c4ed fix: Location dropdown is overflowing #6376 (#6415)
* add `menuPlacement` react-select's props to `getReactSelectProps` to avoid dropdown overflow on small screen.
By default, set to "auto".
2023-01-12 09:29:37 -07:00
Nafees NazikandPeer Richelsen 6414903567 fix: guess correct timezone when onboarding (#6430)
* fix: guess correct timezone

* fix: redirect if on boarded

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-12 16:17:57 +00:00
e62d8b4596 fix: long words break UI (#6410)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-12 09:05:20 -07:00
Carina WollendorferandCarinaWolli e27b1b1bc2 Removes 'Test Action' button in workflows (#6418)
* comment out test action feature

* code clean up

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-12 09:03:30 -07:00
zomars 9507138f29 Labeler fixes 2023-01-12 08:52:21 -07:00
GitStart-Cal.comandgitstart-calcom e5fa202a09 CALCOM-6398 - [CAL-749] remove ethSignature from query if unused (#6425)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-01-12 14:55:00 +01:00
Udit TakkarandAlex van Andel 447098bc97 fix: use i18n properly in utils (#6363)
* fix: use i18n properly in utils

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

* chore

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: Alex van Andel <me@alexvanandel.com>
2023-01-12 05:22:55 +00:00
Udit Takkar dc5e042cdd fix: remove flex-nowrap (#6416)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-12 01:24:39 +00:00
Leo Giovanetti 92fb365b54 Fix for Request Email Reminder (#6421)
* Fix

* Selecting id
2023-01-11 22:12:11 -03:00
161f5c7000 Adds "Upgrade" paywall to Workflow SMS features instead of disabling them (#6412)
* adds upgrade badge to dropdown

* add space between batch and check

* add upgrade tool tip

* fix import

* code clean up

* fix type error

* fix type error

* fix type errors

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-11 18:41:45 +00:00
c8744768c4 Refactored buildSlots (#6389)
* Refactored buildSlots

* Added typing for computedAvailableSlots

* Adds very simple boundary logic to make sure slots start at the right times

* Logic was cutting off too early

* Woops, should be eventLength, not frequency

* Expect 12:15 slots as availability is till 12:30 for booking

* Deliberately skip test for testing purposes

* slotInterval overrides intervals, but not duration

* Fix calendar mocking

* Another 45m length

* Unskip :(

* Fix test

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-01-11 17:33:34 +00:00
Udit TakkarandPeer Richelsen b34a568a93 fix: show recordings even to free plan users (#6411)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-11 09:58:50 -07:00
sean-brydon a669403604 Fix avatars not overlapping (#6405)
* Fix avatars being weird

* Fix useravatar import

* Fix type error

* Fixing spacing
2023-01-11 15:00:46 +00:00
Peer Richelsen 7b2bfc2a64 added GITHUB_API_REPO_TOKEN for console (#6408) 2023-01-11 14:47:37 +00:00
Alex van Andel 7655c0c867 Testing in isolation 2023-01-11 13:24:02 +00:00
Nafees Nazikandsean-brydon 6fbabb4b26 fix: remove border (#6391)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-01-11 10:28:57 +00:00
sean-brydon edfc9ff082 Fixes ultrawide breaking booking limits selects (#6396) 2023-01-11 10:16:04 +00:00
Leo Giovanetti 59a9a77e3f Fixes cancelling all recurring events (#6384) 2023-01-10 14:09:28 -07:00
Joe Au-Yeungandkodiakhq[bot] 8e756d7648 Fix custom event names in emails (#6375)
* Fix What for normal bookings

* Fix subject on bookings with seats

* WIP

* Change more strings

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-10 18:32:50 +00:00
Udit Takkarandsean-brydon 5763c1c2f1 fix: use default length in form (#6378)
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: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-01-10 17:25:09 +00:00
Alex van Andel 6c5ae0b4d9 Fixed default value for ordering correctly (#6372) 2023-01-10 17:06:53 +01:00
9cb9ce2e42 Chor/bulk UI migration (#6367)
* Big bulk commit

* Switch and settings toggle

* Popover Kbar Timezones

* Image Uploader

* Fix core export

* Meta

* Swatch

* Remove shell

* Fix shell imports

* Moved ShellSubHeading component to UI, to prevent recursive imports from shell component

* Removed shell from ui ts config since shell doesnt have a dependency on shell anymore.

Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: Omar López <zomars@me.com>
2023-01-10 15:39:29 +00:00
Omar LópezandCrowdin Bot 018be43017 New Crowdin translations by Github Action (#6365)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-01-10 08:11:59 -07:00
zomars 4f720c8c9b Update crowdin.yml 2023-01-10 08:05:14 -07:00
Udit TakkarandPeer Richelsen 06e1e113f0 fix: margin and padding (#6358)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-10 12:44:30 +00:00
sean-brydonandOmar López 3fc6f38da9 Chor/more UI migrations (#6353)
* Move tabs to components

* Lots of form components

* Update apps/web/components/apps/layouts/InstalledAppsLayout.tsx

Co-authored-by: Omar López <zomars@me.com>
2023-01-10 12:25:39 +00:00
Omar López 8e8c96c267 v2.4.6
release.yml / release (push) Canceled after 0s
2023-01-09 20:26:21 -07:00
github-actions[bot]andCrowdin Bot fab765c5ee New Crowdin translations by Github Action (#6349)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-01-09 20:25:22 -07:00
zomars 89ed07a097 Update crowdin.yml 2023-01-09 20:17:03 -07:00
zomars d7d73d2caf Update crowdin.yml 2023-01-09 20:15:53 -07:00
zomars aa92730e28 Fix labeler 2023-01-09 19:52:02 -07:00
430430e0cd Feedback of reusable wysiwyg editor (#6357)
* make text editor reusable

* use dropdown component for block type

* remove ring when clicking on block type

* allow excluding items in toolbar

* refactor code

* make AddVariableDropdown reusable

* fixed missed translations

* fixes variable translation issue

* change missed translation variables

* make AddVariablesDropdown reusable

* reorder block types in dropdown

* remove not needed prop

* code clean up

* center add variable dropwdown icon

* remove rounded borders in dropwdown on hover and active

* center icons

* fix tool bar items dropdown

* add constant for dynamic text variables

* remove not needed variables (added from merge)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2023-01-09 19:22:17 -07:00
62713940b0 Chor/v2 more tidyup - Card - link icon button - tooltip (#6312)
* Card

* Step card

* Already in components

* Link Icon Button

* Tooltip

* Fix types

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-09 19:22:04 -07:00
75aef09338 Google Meet - installable app (#5904)
* Abstract app category navigation

* Send key schema to frontend

Co-authored-by: Omar López <zomars@users.noreply.github.com>

* Render keys for apps on admin

* Add enabled col for apps

* Save app keys to DB

* Add checks for admin role

* Abstract setup components

* Add AdminAppsList to setup wizard

* Migrate to v10 tRPC

* Default hide keys

* Display enabled apps

* Merge branch 'main' into admin-apps-ui

* Toggle calendars

* WIP

* Add params and include AppCategoryNavigation

* Refactor getEnabledApps

* Add warning for disabling apps

* Fallback to cal video when a video app is disabled

* WIP send disabled email

* Send email to all users of  event types with payment app

* Disable Stripe when app is disabled

* Disable apps in event types

* Send email to users on disabled apps

* Send email based on what app was disabled

* WIP type fix

* Disable navigation to apps list if already setup

* UI import fixes

* Waits for session data before redirecting

* Updates admin seeded password

To comply with admin password requirements

* Update yarn.lock

* Flex fixes

* Adds admin middleware

* Clean up

* WIP

* WIP

* NTS

* Add dirName to app metadata

* Upsert app if not in db

* Upsert app if not in db

* Add dirName to app metadata

* Add keys to app packages w/ keys

* Merge with main

* Toggle show keys & on enable

* Fix empty keys

* Fix lark calendar metadata

* Fix some type errors

* Fix Lark metadata & check for category when upserting

* More type fixes

* Fix types & add keys to google cal

* WIP

* WIP

* WIP

* More type fixes

* Fix type errors

* Fix type errors

* More type fixes

* More type fixes

* More type fixes

* Feedback

* Fixes default value

* Feedback

* Migrate credential invalid col default value "false"

* Upsert app on saving keys

* Clean up

* Validate app keys on frontend

* Add nonempty to app keys schemas

* Add web3

* Listlocale filter on categories / category

* Grab app metadata via category or categories

* Show empty screen if no apps are enabled

* Fix type checks

* Fix type checks

* Fix type checks

* Fix type checks

* Fix type checks

* Fix type checks

* Replace .nonempty() w/ .min(1)

* Fix type error

* Address feedback

* Draft Google Meet install button

* Add install button and warning dialog

* WIP

* WIP

* Display warning when Meet is selected

* Display Google Meet warning on email to organizer

* Fix email

* Fix type errors

* Fix type error

* Add connected account component

* Add warning message

* Address comments

* Address feedback

* Clean up & add MeetLocationType

* Use useApp hook

* Translate to new API approach

* Remove console.log

* Refactor

* Fix missing backup Cal video link

* WIP

* Address feedback

* Update submodules

* Feedback

* Submodule sync

Co-authored-by: Omar López <zomars@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2023-01-10 02:01:57 +00:00
4e65c30e18 fix: Parameterize Stripe Payment Fee configs (#6024)
* Parameterize Stripe Payment Fee configs

* Restore constant values for Payment Fee params

* Restore constant values for all Payment Fee params

* Remove STRIPE_ prefix on payment fee vars

* Cast Payment Fee params as Number

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2023-01-09 18:50:49 -07:00
8b873e2828 Adding a bunch of performance markers (#6303)
* Adding a bunch of performance markers

* Update packages/lib/server/perfObserver.ts

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-09 18:42:46 -07:00
zomars e70d179628 Update website 2023-01-09 18:31:38 -07:00
Nafees Nazik ca9a836300 fix: remove padding from username field (#6356) 2023-01-09 19:35:00 +01:00
Alex van Andel abdf72dc0f Fixup yarn.lock 2023-01-09 14:36:49 +00:00
dependabot[bot]andBailey Pumfleet 227cd165f6 Bump json5 from 1.0.1 to 1.0.2 (#6351)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-01-09 14:19:51 +00:00
a0135cdc59 Make WYSIWYG editor reusable (#6293)
* make text editor reusable

* use dropdown component for block type

* remove ring when clicking on block type

* allow excluding items in toolbar

* refactor code

* make AddVariableDropdown reusable

* fixed missed translations

* fixes variable translation issue

* change missed translation variables

* make AddVariablesDropdown reusable

* reorder block types in dropdown

* remove not needed prop

* code clean up

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-09 14:15:11 +00:00
112d504148 Chor/v2 UI tidy - Dialog & Toasts (#6307)
* Dialog Tidyup

* Move toasts

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2023-01-09 13:57:12 +00:00
Peer Richelsen d275930732 link to event-types in settings to fix cal.com usage (#6348) 2023-01-09 13:15:10 +01:00
Alex van Andel 97e33bdd7e Set bg-white to secondary variant (#6339) 2023-01-08 17:50:37 +01:00
Hariom Balhara 3f13791faf Add app-store installed page navigationt ests (#6333) 2023-01-08 09:29:20 +01:00
Omar LópezandAlex van Andel 359cfc4be0 Replaces mdx-remote with markdown-it (#6326)
* Replaces mdx-remote with react-markdown

* Replaces react-markdown with already available markdown-it

* Renames readme to description

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-01-08 00:05:35 +00:00
Alex van AndelandPeer Richelsen 1ad6bba0ab set useQueryParam to false for app categories (#6330)
* set useQueryParam to false for app categories

* New param 'useQueryParam' for AppCategoryNavigation/AdminAppsList

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-07 23:21:21 +00:00
Alex van Andel 6dc4211d81 Fixes hydration error, uses useTypedQuery for tabName (#6327) 2023-01-07 22:42:30 +00:00
Alex van Andel e5f0452728 Don't initialize KBar when unauthed (#6328)
* Don't initialize KBar when unauthed

* Don't render trigger when query is undefined
2023-01-07 22:31:15 +00:00
zomars d95bba63f4 Submodule sync 2023-01-07 11:19:16 -07:00
zomars 6f48869021 Add missing slash to custom import 2023-01-07 10:17:38 -07:00
Jeff Loiselle 845bfae887 adds event types to kbar (#6210) 2023-01-07 10:06:53 -07:00
zomars 3fe02fba26 Update basic.e2e.ts 2023-01-07 09:55:30 -07:00
zomars 4ca5ca9fe2 Update hash-my-url.e2e.ts 2023-01-07 09:50:02 -07:00
Hariom BalharaandPeer Richelsen 364e187080 Fix/Next.js-13/Circular Dependency in Routing Forms (#6321)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-07 15:50:28 +00:00
Peer Richelsen b1193eef1e Update README.md 2023-01-07 16:27:56 +01:00
Peer Richelsen e110f12cf1 Update README.md 2023-01-07 16:26:47 +01:00
Peer Richelsen 022b513935 Update package.json 2023-01-07 11:55:50 +01:00
Jeff LoiselleandPeer Richelsen 010f39a7a4 adds render.com url constant (#6287)
* adds render.com url constant

* fixes webapp_url constant

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-07 10:28:02 +00:00
Peer Richelsen 6b6eed66eb fixed mobile sidebar in settings (#6317) 2023-01-07 10:10:35 +00:00
Udit Takkar c214e01f03 fix: use overflow-auto (#6295)
* fix: use overflow-auto

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

* fix: scroll issues with too many filters

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

* chore: merge conflict

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

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-07 11:00:11 +01:00
Carina WollendorferandCarinaWolli a28ac3634d Add start and end time of rescheduled booking to webhook payload (#6297)
* add start and end time of rescheduled booking

* fix type error

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-06 13:37:06 -07:00
zomars a0709fac88 Circular dependecy hotfix 2023-01-06 13:15:34 -07:00
Alex van Andel e5b6a3332e Feature/new design radio area group (#6289)
* Refactor of the RadioAreaGroup component

* Move RadioAreaGroup outside of v2/core

* Use useTypedQuery

* Removed unused (broken) imports

* Fix type for Select
2023-01-06 20:05:23 +00:00
Jeff Loiselle 979686920a fixes codespace login msg spelling (#6288) 2023-01-06 12:37:03 -07:00
zomars 793850f3aa Update console 2023-01-06 11:10:17 -07:00
Nafees NazikandPeer Richelsen fa9d72fc0a fix: flash of untranslated title in settings/my-account/profile (#6163)
* fix: flash of untranslated content

* feat: extract loading into a variable

* fix: use optional chaining because typescript complaining

* feat: render Meta component even if not loading for seo purpose

* fix: received an array with more than 1 element as
children warning

* fix: changes

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-06 10:35:32 -07:00
zomars 0d485ffed6 Updates website 2023-01-06 10:18:18 -07:00
Peer Richelsen 9a2e55d528 revert version change 2023-01-06 16:59:26 +01:00
github-actions[bot]andCrowdin Bot 81ecbb86a5 New Crowdin translations by Github Action (#6308)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-01-06 16:58:09 +01:00
Peer Richelsen 2cd816932b Update package.json 2023-01-06 16:55:01 +01:00
Peer Richelsenandzomars d83411162d Upgrade to next 13.1 (#6265)
Co-authored-by: zomars <zomars@me.com>
2023-01-06 12:13:56 +00:00
Nafees NazikandPeer Richelsen c9c54db3a1 fix: remove duplicate font preload (#6291)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-06 12:35:48 +01:00
Udit Takkarandkodiakhq[bot] d100600789 feat: update team schema, update team query (#6294)
team appearance section

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: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-06 10:55:57 +00:00
636 changed files with 8718 additions and 7188 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
🚀 Welcome to Cal.com! Try typing 'yarn dx' to get a quick dev envionment.
+11 -1
View File
@@ -31,11 +31,14 @@ NEXT_PUBLIC_CONSOLE_URL='http://localhost:3004'
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js' NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
# To enable SAML login, set both these variables # To enable SAML login, set both these variables
# @see https://github.com/calcom/cal.com/tree/main/packages/ee#setting-up-saml-login # @see https://github.com/calcom/cal.com/tree/main/packages/features/ee#setting-up-saml-login
# SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml" # SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml"
SAML_DATABASE_URL= SAML_DATABASE_URL=
# SAML_ADMINS='pro@example.com' # SAML_ADMINS='pro@example.com'
SAML_ADMINS= SAML_ADMINS=
# NEXT_PUBLIC_HOSTED_CAL_FEATURES=1
NEXT_PUBLIC_HOSTED_CAL_FEATURES=
# If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line. # If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line.
# @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js # @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
# PGSSLMODE='no-verify' # PGSSLMODE='no-verify'
@@ -79,6 +82,7 @@ SEND_FEEDBACK_EMAIL=
# Used for email reminders in workflows and internal sync services # Used for email reminders in workflows and internal sync services
SENDGRID_API_KEY= SENDGRID_API_KEY=
SENDGRID_EMAIL= SENDGRID_EMAIL=
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
# Twilio # Twilio
# Used to send SMS reminders in workflows # Used to send SMS reminders in workflows
@@ -86,6 +90,7 @@ TWILIO_SID=
TWILIO_TOKEN= TWILIO_TOKEN=
TWILIO_MESSAGING_SID= TWILIO_MESSAGING_SID=
TWILIO_PHONE_NUMBER= TWILIO_PHONE_NUMBER=
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
NEXT_PUBLIC_SENDER_ID= NEXT_PUBLIC_SENDER_ID=
TWILIO_VERIFY_SID= TWILIO_VERIFY_SID=
@@ -102,6 +107,8 @@ STRIPE_TEAM_MONTHLY_PRICE_ID=
STRIPE_WEBHOOK_SECRET= STRIPE_WEBHOOK_SECRET=
STRIPE_PRIVATE_KEY= STRIPE_PRIVATE_KEY=
STRIPE_CLIENT_ID= STRIPE_CLIENT_ID=
PAYMENT_FEE_FIXED=
PAYMENT_FEE_PERCENTAGE=
# Use for internal Public API Keys and optional # Use for internal Public API Keys and optional
API_KEY_PREFIX=cal_ API_KEY_PREFIX=cal_
@@ -147,3 +154,6 @@ SENTRY_IGNORE_API_RESOLUTION_ERROR=
NEXT_PUBLIC_APP_NAME="Cal.com" NEXT_PUBLIC_APP_NAME="Cal.com"
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com" NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc." NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
# Set this to true in to disable new signups
# NEXT_PUBLIC_DISABLE_SIGNUP=true
NEXT_PUBLIC_DISABLE_SIGNUP=
-6
View File
@@ -1,9 +1,3 @@
"♻️ autoupdate":
- apps/web/public/static/locales/**/*.json
automerge:
- apps/web/public/static/locales/**/*.json
"❗️ migrations": "❗️ migrations":
- packages/prisma/migrations/**/migration.sql - packages/prisma/migrations/**/migration.sql
+3
View File
@@ -9,6 +9,8 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
required-checks:
uses: calcom/cal.com/.github/workflows/required-checks.yml@main
synchronize-with-crowdin: synchronize-with-crowdin:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -21,6 +23,7 @@ jobs:
with: with:
upload_translations: true upload_translations: true
download_translations: true download_translations: true
pull_request_labels: "♻️ autoupdate, automerge"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
+1
View File
@@ -7,6 +7,7 @@ on:
paths: paths:
- apps/web/** - apps/web/**
- packages/embeds/** - packages/embeds/**
workflow_call:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
+1 -2
View File
@@ -1,13 +1,12 @@
name: E2E test name: E2E test
on: on:
push:
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
pull_request_target: # So we can test on forks pull_request_target: # So we can test on forks
branches: branches:
- main - main
paths: paths:
- apps/web/** - apps/web/**
- packages/** - packages/**
workflow_call:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
+2
View File
@@ -14,3 +14,5 @@ jobs:
- uses: actions/labeler@v4 - uses: actions/labeler@v4
with: with:
repo-token: "${{ secrets.GITHUB_TOKEN }}" repo-token: "${{ secrets.GITHUB_TOKEN }}"
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
sync-labels: ""
+1 -2
View File
@@ -1,10 +1,9 @@
name: "Meta Workflow: Require Conditional Status Checks" name: "Meta Workflow: Require Conditional Status Checks"
on: on:
push:
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
pull_request: pull_request:
branches: branches:
- main - main
workflow_call:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
+3 -2
View File
@@ -1,13 +1,14 @@
name: Unit tests name: Unit tests
on: on:
push:
branches: [ feat/routing-forms/reporting ] # TODO: Remove this after merged in main
pull_request_target: # So we can test on forks pull_request_target: # So we can test on forks
branches: branches:
- main - main
paths: paths:
- apps/web/** - apps/web/**
- packages/** - packages/**
workflow_run:
workflows: [Crowdin Action]
types: [completed]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
+6
View File
@@ -80,3 +80,9 @@ apps/storybook/build-storybook.log
# Snaplet # Snaplet
.snaplet/snapshots .snaplet/snapshots
.snaplet/structure.d.ts .snaplet/structure.d.ts
# Submodules
.gitmodules
apps/api
apps/website
apps/console
-12
View File
@@ -1,12 +0,0 @@
[submodule "apps/console"]
path = apps/console
url = https://github.com/calcom/console.git
branch = main
[submodule "apps/api"]
path = apps/api
url = https://github.com/calcom/api.git
branch = main
[submodule "apps/website"]
path = apps/website
url = https://github.com/calcom/website.git
branch = main
+18 -11
View File
@@ -305,6 +305,12 @@ Currently Vercel Pro Plan is required to be able to Deploy this application with
See the [roadmap project](https://cal.com/roadmap) for a list of proposed features (and known issues). You can change the view to see planned tagged releases. See the [roadmap project](https://cal.com/roadmap) for a list of proposed features (and known issues). You can change the view to see planned tagged releases.
<!-- RORADMAP -->
## Repo Activity
<img width="100%" src="https://repobeats.axiom.co/api/embed/6bfca2f20f39738048b6e70ca205efde46352c3d.svg" />
<!-- CONTRIBUTING --> <!-- CONTRIBUTING -->
## Contributing ## Contributing
@@ -315,6 +321,17 @@ Please see our [contributing guide](/CONTRIBUTING.md).
We have a list of [help wanted](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) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process. We have a list of [help wanted](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) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
<!-- TRANSLATIONS -->
### Translations
Don't code but still want to contribute? Join our [slack](https://cal.com/slack) and join the [#i18n channel](https://calendso.slack.com/archives/C02BY67GMMW) and let us know what language you want to translate.
![ar translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ar&style=flat&logo=crowdin&query=%24.progress.0.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![bg translation](https://img.shields.io/badge/dynamic/json?color=blue&label=bg&style=flat&logo=crowdin&query=%24.progress.1.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![cs translation](https://img.shields.io/badge/dynamic/json?color=blue&label=cs&style=flat&logo=crowdin&query=%24.progress.2.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![de translation](https://img.shields.io/badge/dynamic/json?color=blue&label=de&style=flat&logo=crowdin&query=%24.progress.3.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![el translation](https://img.shields.io/badge/dynamic/json?color=blue&label=el&style=flat&logo=crowdin&query=%24.progress.4.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![en translation](https://img.shields.io/badge/dynamic/json?color=blue&label=en&style=flat&logo=crowdin&query=%24.progress.5.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![es translation](https://img.shields.io/badge/dynamic/json?color=blue&label=es&style=flat&logo=crowdin&query=%24.progress.6.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![es-419 translation](https://img.shields.io/badge/dynamic/json?color=blue&label=es-419&style=flat&logo=crowdin&query=%24.progress.7.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![fr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=fr&style=flat&logo=crowdin&query=%24.progress.8.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![he translation](https://img.shields.io/badge/dynamic/json?color=blue&label=he&style=flat&logo=crowdin&query=%24.progress.9.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![hu translation](https://img.shields.io/badge/dynamic/json?color=blue&label=hu&style=flat&logo=crowdin&query=%24.progress.10.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![it translation](https://img.shields.io/badge/dynamic/json?color=blue&label=it&style=flat&logo=crowdin&query=%24.progress.11.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![ja translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ja&style=flat&logo=crowdin&query=%24.progress.12.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![ko translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ko&style=flat&logo=crowdin&query=%24.progress.13.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![nl translation](https://img.shields.io/badge/dynamic/json?color=blue&label=nl&style=flat&logo=crowdin&query=%24.progress.14.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![no translation](https://img.shields.io/badge/dynamic/json?color=blue&label=no&style=flat&logo=crowdin&query=%24.progress.15.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![pl translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pl&style=flat&logo=crowdin&query=%24.progress.16.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![pt translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pt&style=flat&logo=crowdin&query=%24.progress.17.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![pt-BR translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pt-BR&style=flat&logo=crowdin&query=%24.progress.18.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![ro translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ro&style=flat&logo=crowdin&query=%24.progress.19.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&logo=crowdin&query=%24.progress.20.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![sr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=sr&style=flat&logo=crowdin&query=%24.progress.21.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![sv translation](https://img.shields.io/badge/dynamic/json?color=blue&label=sv&style=flat&logo=crowdin&query=%Line truncated
## Integrations ## Integrations
### Obtaining the Google API Credentials ### Obtaining the Google API Credentials
@@ -394,17 +411,6 @@ following
9. Click the "Save" button at the bottom footer. 9. Click the "Save" button at the bottom footer.
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts. 10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
### Obtaining Vital API Keys
1. Open [Vital](https://tryvital.io/) and click Get API Keys.
1. Create a team with the team name you desire
1. Head to the configuration section on the sidebar of the dashboard
1. Click on API keys and you'll find your sandbox `api_key`.
1. Copy your `api_key` to `VITAL_API_KEY` in the .env.appStore file.
1. Open [Vital Webhooks](https://app.tryvital.io/team/{team_id}/webhooks) and add `<CALCOM BASE URL>/api/integrations/vital/webhook` as webhook for connected applications.
1. Select all events for the webhook you interested, e.g. `sleep_created`
1. Copy the webhook secret (`sec...`) to `VITAL_WEBHOOK_SECRET` in the .env.appStore file.
## Workflows ## Workflows
### Setting up SendGrid for Email reminders ### Setting up SendGrid for Email reminders
@@ -414,6 +420,7 @@ following
3. Copy API key to your .env file into the SENDGRID_API_KEY field 3. Copy API key to your .env file into the SENDGRID_API_KEY field
4. Go to Settings -> Sender Authentication and verify a single sender 4. Go to Settings -> Sender Authentication and verify a single sender
5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field 5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field
6. Add your custom sender name to the .env file into the NEXT_PUBLIC_SENDGRID_SENDER_NAME field (fallback is Cal.com)
### Setting up Twilio for SMS reminders ### Setting up Twilio for SMS reminders
Submodule apps/api deleted from 9c23a8e5ab.
Submodule apps/console deleted from 2d577dd9ed.
+1 -1
View File
@@ -17,7 +17,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"iframe-resizer-react": "^1.1.0", "iframe-resizer-react": "^1.1.0",
"next": "^12.3.1", "next": "^13.1.1",
"nextra": "^1.1.0", "nextra": "^1.1.0",
"nextra-theme-docs": "^1.2.2", "nextra-theme-docs": "^1.2.2",
"react": "^18.2.0", "react": "^18.2.0",
+3 -3
View File
@@ -31,7 +31,7 @@ All apps can be found under `packages/app-store`. In this folder is `_example` w
| ├──index.ts | ├──index.ts
| ├──package.json | ├──package.json
| ├──.env.example | ├──.env.example
| ├──README.mdx | ├──DESCRIPTION.md
``` ```
## Getting Started ## Getting Started
@@ -48,9 +48,9 @@ Under the `/components` folder, this is where the install button for your app sh
The `/lib` folder is where the functions of your app live. For example, when creating a booking with a MS Teams link the function to make the call to grab the link lives in the `/lib` folder. Export your endpoints in an `index.ts` file under `/lib` folder and import them in your main `index.ts` file. The `/lib` folder is where the functions of your app live. For example, when creating a booking with a MS Teams link the function to make the call to grab the link lives in the `/lib` folder. Export your endpoints in an `index.ts` file under `/lib` folder and import them in your main `index.ts` file.
On the app store page you can customize your apps description by adding a markdown file called `README.mdx`. If you do not add one then the description from you `package.json` will be used instead. On the app store page you can customize your apps description by adding a markdown file called `DESCRIPTION.md`. If you do not add one then the description from you `package.json` will be used instead.
The `/static` folder is where you can store your app icon and any images that your `README.mdx` may use. The `/static` folder is where you can store your app icon and any images that your `DESCRIPTION.md` may use.
## Adding Your App to the App Store ## Adding Your App to the App Store
To render your app on the app store page, go to `packages/app-store/index.ts`. Import your app into the file and add it to the `appStore` object. To render your app on the app store page, go to `packages/app-store/index.ts`. Import your app into the file and add it to the `appStore` object.
+1
View File
@@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules node_modules
storybook-static
dist dist
dist-ssr dist-ssr
*.local *.local
+4 -2
View File
@@ -11,13 +11,15 @@ module.exports = {
"@storybook/addon-links", "@storybook/addon-links",
"@storybook/addon-essentials", "@storybook/addon-essentials",
"@storybook/addon-interactions", "@storybook/addon-interactions",
"storybook-addon-rtl-direction",
"storybook-react-i18next", "storybook-react-i18next",
{ "storybook-addon-next",
/*{
name: "storybook-addon-next", name: "storybook-addon-next",
options: { options: {
nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"), nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"),
}, },
}, },*/
], ],
framework: "@storybook/react", framework: "@storybook/react",
core: { core: {
+45
View File
@@ -0,0 +1,45 @@
const withBundleAnalyzer = require("@next/bundle-analyzer");
const withTM = require("next-transpile-modules")([
"@calcom/app-store",
"@calcom/dayjs",
"@calcom/emails",
"@calcom/trpc",
"@calcom/embed-core",
"@calcom/embed-react",
"@calcom/features",
"@calcom/lib",
"@calcom/prisma",
"@calcom/ui",
]);
const glob = require("glob");
const plugins = [];
plugins.push(withTM, withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
/** @type {import("next").NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
domains: ["www.datocms-assets.com"],
formats: ["image/avif", "image/webp"],
},
typescript: {
ignoreBuildErrors: true,
},
experimental: { images: { allowFutureImage: true } },
eslint: {
ignoreDuringBuilds: true,
},
webpack: (config, { isServer }) => {
if (!isServer) {
// don't resolve 'fs' module on the client to prevent this error on build --> Error: Can't resolve 'fs'
config.resolve.fallback = {
fs: false,
};
}
return config;
},
};
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
+3 -2
View File
@@ -20,9 +20,10 @@
"@radix-ui/react-slider": "^1.0.0", "@radix-ui/react-slider": "^1.0.0",
"@radix-ui/react-switch": "^1.0.0", "@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0",
"next": "^12.3.1", "next": "^13.1.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"storybook-addon-rtl-direction": "^0.0.19"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19.6", "@babel/core": "^7.19.6",
+1 -1
View File
@@ -14,7 +14,7 @@
"dependencies": { "dependencies": {
"highlight.js": "^11.6.0", "highlight.js": "^11.6.0",
"isarray": "2.0.5", "isarray": "2.0.5",
"next": "^12.3.1", "next": "^13.1.1",
"openapi-snippet": "^0.13.0", "openapi-snippet": "^0.13.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
+35
View File
@@ -0,0 +1,35 @@
import { ReactNode } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge, ListItemText } from "@calcom/ui";
interface AppListCardProps {
logo?: string;
title: string;
description: string;
actions?: ReactNode;
isDefault?: boolean;
}
export default function AppListCard(props: AppListCardProps) {
const { t } = useLocale();
const { logo, title, description, actions, isDefault } = props;
return (
<div className="p-4">
<div className="flex items-center gap-x-3">
{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 items-center gap-x-2">
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
{isDefault ? <Badge variant="green">{t("default")}</Badge> : null}
</div>
<ListItemText component="p">{description}</ListItemText>
</div>
{actions}
</div>
</div>
);
}
-9
View File
@@ -1,9 +0,0 @@
import { Suspense, SuspenseProps } from "react";
/**
* Wrapper around `<Suspense />` which will render the `fallback` when on server
* Can be simply replaced by `<Suspense />` once React 18 is ready.
*/
export const ClientSuspense = (props: SuspenseProps) => {
return <>{typeof window !== "undefined" ? <Suspense {...props} /> : props.fallback}</>;
};
+10 -10
View File
@@ -504,7 +504,7 @@ const tabs = [
return ( return (
<> <>
<div> <div>
<small className="flex py-4 text-neutral-500"> <small className="flex py-4 text-gray-500">
{t("place_where_cal_widget_appear", { appName: APP_NAME })} {t("place_where_cal_widget_appear", { appName: APP_NAME })}
</small> </small>
</div> </div>
@@ -556,7 +556,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
} }
return ( return (
<> <>
<small className="flex py-4 text-neutral-500">{t("create_update_react_component")}</small> <small className="flex py-4 text-gray-500">{t("create_update_react_component")}</small>
<TextArea <TextArea
data-testid="embed-react" data-testid="embed-react"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>} ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
@@ -597,7 +597,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
<iframe <iframe
ref={ref as typeof ref & MutableRefObject<HTMLIFrameElement>} ref={ref as typeof ref & MutableRefObject<HTMLIFrameElement>}
data-testid="embed-preview" data-testid="embed-preview"
className="border-1 h-[100vh] border" className="h-[100vh] border"
width="100%" width="100%"
height="100%" height="100%"
src={`${WEBAPP_URL}/embed/preview.html?embedType=${embedType}&calLink=${calLink}`} src={`${WEBAPP_URL}/embed/preview.html?embedType=${embedType}&calLink=${calLink}`}
@@ -639,7 +639,7 @@ const ChooseEmbedTypesDialogContent = () => {
<div className="flex items-start"> <div className="flex items-start">
{embeds.map((embed, index) => ( {embeds.map((embed, index) => (
<button <button
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-neutral-100 ltr:mr-2 rtl:ml-2" className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-gray-100 ltr:mr-2 rtl:ml-2"
key={index} key={index}
data-testid={embed.type} data-testid={embed.type}
onClick={() => { onClick={() => {
@@ -650,7 +650,7 @@ const ChooseEmbedTypesDialogContent = () => {
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white"> <div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
{embed.illustration} {embed.illustration}
</div> </div>
<div className="mt-2 font-medium text-neutral-900">{embed.title}</div> <div className="mt-2 font-medium text-gray-900">{embed.title}</div>
<p className="text-sm text-gray-500">{embed.subtitle}</p> <p className="text-sm text-gray-500">{embed.subtitle}</p>
</button> </button>
))} ))}
@@ -827,7 +827,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}> onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
<CollapsibleTrigger <CollapsibleTrigger
type="button" type="button"
className="flex w-full items-center text-base font-medium text-neutral-900"> className="flex w-full items-center text-base font-medium text-gray-900">
<div> <div>
{embedType === "inline" {embedType === "inline"
? "Inline Embed Customization" ? "Inline Embed Customization"
@@ -838,7 +838,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<Icon.FiChevronRight <Icon.FiChevronRight
className={`${ className={`${
isEmbedCustomizationOpen ? "rotate-90 transform" : "" isEmbedCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-neutral-500`} } ml-auto h-5 w-5 text-gray-500`}
/> />
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="text-sm"> <CollapsibleContent className="text-sm">
@@ -1001,11 +1001,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
open={isBookingCustomizationOpen} open={isBookingCustomizationOpen}
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-neutral-900">Cal Booking Customization</div> <div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
<Icon.FiChevronRight <Icon.FiChevronRight
className={`${ className={`${
isBookingCustomizationOpen ? "rotate-90 transform" : "" isBookingCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-neutral-500`} } ml-auto h-5 w-5 text-gray-500`}
/> />
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
@@ -1163,7 +1163,7 @@ export const EmbedButton = <T extends React.ElementType>({
...props ...props
}: EmbedButtonProps<T> & React.ComponentPropsWithoutRef<T>) => { }: EmbedButtonProps<T> & React.ComponentPropsWithoutRef<T>) => {
const router = useRouter(); const router = useRouter();
className = classNames(className, "hidden lg:inline-flex"); className = classNames("hidden lg:inline-flex", className);
const openEmbedModal = () => { const openEmbedModal = () => {
goto(router, { goto(router, {
dialog: "embed", dialog: "embed",
+1 -1
View File
@@ -142,7 +142,7 @@ export default function ImageUploader({
</div> </div>
)} )}
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />} {result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900"> <label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
<input <input
onInput={onInputFile} onInput={onInputFile}
type="file" type="file"
+3 -3
View File
@@ -62,12 +62,12 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
const className = tab.className || ""; const className = tab.className || "";
return ( return (
<Component key={tab.name}> <Component key={tab.name}>
<Link key={tab.name} href={href} {...linkProps}> <Link key={tab.name} href={href} {...linkProps} legacyBehavior>
<a <a
onClick={onClick} onClick={onClick}
className={classNames( className={classNames(
isCurrent isCurrent
? "border-neutral-900 text-neutral-900" ? "border-gray-900 text-gray-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700", : "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium", "group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
className className
@@ -76,7 +76,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
{tab.icon && ( {tab.icon && (
<tab.icon <tab.icon
className={classNames( className={classNames(
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500", isCurrent ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block" "-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
)} )}
aria-hidden="true" aria-hidden="true"
+2 -1
View File
@@ -1,6 +1,7 @@
import React, { ComponentProps } from "react"; import React, { ComponentProps } from "react";
import { ErrorBoundary, Icon, Shell } from "@calcom/ui"; import Shell from "@calcom/features/shell/Shell";
import { ErrorBoundary, Icon } from "@calcom/ui";
import NavTabs from "./NavTabs"; import NavTabs from "./NavTabs";
+13 -3
View File
@@ -6,12 +6,13 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton } from "@calcom/app-store/components"; import { InstallAppButton } from "@calcom/app-store/components";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration"; import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired"; import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
import Shell from "@calcom/features/shell/Shell";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants"; import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } 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 { trpc } from "@calcom/trpc/react";
import { App as AppType } from "@calcom/types/App"; import { App as AppType } from "@calcom/types/App";
import { Button, Icon, Shell, showToast, SkeletonButton, SkeletonText, HeadSeo } from "@calcom/ui"; import { Button, Icon, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
const Component = ({ const Component = ({
name, name,
@@ -33,6 +34,7 @@ const Component = ({
privacy, privacy,
isProOnly, isProOnly,
images, images,
isTemplate,
}: Parameters<typeof App>[0]) => { }: Parameters<typeof App>[0]) => {
const { t } = useLocale(); const { t } = useLocale();
const hasImages = images && images.length > 0; const hasImages = images && images.length > 0;
@@ -98,11 +100,18 @@ const Component = ({
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1> <h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
</div> </div>
<h2 className="text-sm font-medium text-gray-600"> <h2 className="text-sm font-medium text-gray-600">
<Link href={`categories/${categories[0]}`}> <Link
<a className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">{categories[0]}</a> href={`categories/${categories[0]}`}
className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">
{categories[0]}
</Link>{" "} </Link>{" "}
• {t("published_by", { author })} • {t("published_by", { author })}
</h2> </h2>
{isTemplate && (
<Badge variant="red" className="mt-4">
Template - Available in Dev Environment only for testing
</Badge>
)}
</header> </header>
</div> </div>
{!appCredentials.isLoading ? ( {!appCredentials.isLoading ? (
@@ -307,6 +316,7 @@ export default function App(props: {
licenseRequired: AppType["licenseRequired"]; licenseRequired: AppType["licenseRequired"];
isProOnly: AppType["isProOnly"]; isProOnly: AppType["isProOnly"];
images?: string[]; images?: string[];
isTemplate?: boolean;
}) { }) {
const { t } = useLocale(); const { t } = useLocale();
@@ -13,10 +13,10 @@ import {
EmptyScreen, EmptyScreen,
Icon, Icon,
List, List,
ShellSubHeading,
showToast, showToast,
AppSkeletonLoader as SkeletonLoader, AppSkeletonLoader as SkeletonLoader,
Switch, Switch,
ShellSubHeading,
} from "@calcom/ui"; } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
@@ -184,9 +184,7 @@ function ConnectedCalendarsList(props: Props) {
<div className="border-t border-gray-200"> <div className="border-t border-gray-200">
{!fromOnboarding && ( {!fromOnboarding && (
<> <>
<p className="px-4 pt-4 text-sm text-neutral-500"> <p className="px-4 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
{t("toggle_calendars_conflict")}
</p>
<ul className="space-y-2 p-4"> <ul className="space-y-2 p-4">
{item.calendars.map((cal) => ( {item.calendars.map((cal) => (
<CalendarSwitch <CalendarSwitch
@@ -330,7 +328,10 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
})} })}
description={t(`no_category_apps_description_calendar`)} description={t(`no_category_apps_description_calendar`)}
buttonRaw={ buttonRaw={
<Button color="secondary" href="/apps/categories/calendar"> <Button
color="secondary"
data-testid="connect-calendar-apps"
href="/apps/categories/calendar">
{t(`connect_calendar_apps`)} {t(`connect_calendar_apps`)}
</Button> </Button>
} }
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { ReactNode, useEffect, useState } from "react"; import { ReactNode, useEffect, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui"; import { Badge, Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
@@ -19,6 +19,7 @@ function IntegrationListItem(props: {
destination?: boolean; destination?: boolean;
separate?: boolean; separate?: boolean;
invalidCredential?: boolean; invalidCredential?: boolean;
isTemplate?: boolean;
}): JSX.Element { }): JSX.Element {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
@@ -50,8 +51,13 @@ function IntegrationListItem(props: {
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}> <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} />} {props.logo && <img className="h-11 w-11" src={props.logo} alt={title} />}
<div className="flex-grow truncate pl-2"> <div className="flex-grow truncate pl-2">
<ListItemTitle component="h3"> <ListItemTitle component="h3" className="flex ">
<Link href={"/apps/" + props.slug}>{props.name || title}</Link> <Link href={"/apps/" + props.slug}>{props.name || title}</Link>
{props.isTemplate && (
<Badge variant="red" className="ml-4">
Template
</Badge>
)}
</ListItemTitle> </ListItemTitle>
<ListItemText component="p">{props.description}</ListItemText> <ListItemText component="p">{props.description}</ListItemText>
{/* Alert error that key stopped working. */} {/* Alert error that key stopped working. */}
@@ -2,19 +2,9 @@ import { useSession } from "next-auth/react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { ComponentProps } from "react"; import React, { ComponentProps } from "react";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen, Icon, Shell, HorizontalTabs, HorizontalTabItemProps } from "@calcom/ui"; import { EmptyScreen, Icon } from "@calcom/ui";
const tabs: HorizontalTabItemProps[] = [
{
name: "app_store",
href: "/apps",
},
{
name: "installed_apps",
href: "/apps/installed",
},
];
type AppsLayoutProps = { type AppsLayoutProps = {
children: React.ReactNode; children: React.ReactNode;
@@ -30,11 +20,8 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
if (session.status === "loading") return <></>; if (session.status === "loading") return <></>;
return ( return (
<Shell {...rest} actions={actions?.("hidden sm:block")}> <Shell {...rest} actions={actions?.("block")}>
<div className="flex flex-col xl:flex-row"> <div className="flex flex-col xl:flex-row">
<div className="block lg:hidden">
<HorizontalTabs tabs={tabs} actions={actions?.("ml-6 mr-4 mt-3 block sm:hidden")} />
</div>
<main className="w-full"> <main className="w-full">
{emptyStore ? ( {emptyStore ? (
<EmptyScreen <EmptyScreen
@@ -2,10 +2,10 @@ import React, { ComponentProps } from "react";
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation"; import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
import { InstalledAppVariants } from "@calcom/app-store/utils"; import { InstalledAppVariants } from "@calcom/app-store/utils";
import Shell from "@calcom/features/shell/Shell";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Icon, Shell } from "@calcom/ui"; import { Icon } from "@calcom/ui";
import type { HorizontalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/HorizontalTabItem"; import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
import type { VerticalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/VerticalTabItem";
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [ const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{ {
@@ -44,13 +44,15 @@ export default function InstalledAppsLayout({
children, children,
...rest ...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) { }: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
const variant: typeof InstalledAppVariants[number] = "payment";
const query = trpc.viewer.integrations.useQuery({ const query = trpc.viewer.integrations.useQuery({
variant: InstalledAppVariants.payment, variant,
onlyInstalled: true, onlyInstalled: true,
}); });
let actualTabs = tabs; let actualTabs = tabs;
if (query.data?.items.length === 0) { if (query.data?.items.length === 0) {
actualTabs = tabs.filter((tab) => tab.name !== InstalledAppVariants.payment); actualTabs = tabs.filter((tab) => tab.name !== variant);
} }
return ( return (
@@ -3,7 +3,8 @@ import { useRouter } from "next/router";
import React, { ComponentProps, useEffect } from "react"; import React, { ComponentProps, useEffect } from "react";
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout"; import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
import { ErrorBoundary, Shell } from "@calcom/ui"; import Shell from "@calcom/features/shell/Shell";
import { ErrorBoundary } from "@calcom/ui";
import { UserPermissionRole } from ".prisma/client"; import { UserPermissionRole } from ".prisma/client";
@@ -6,7 +6,7 @@ import classNames from "@lib/classNames";
function SkeletonLoader() { function SkeletonLoader() {
return ( return (
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden"> <ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
+24 -24
View File
@@ -87,7 +87,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
slug: eventTypeSlug, slug: eventTypeSlug,
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */ /** Treat as recurring only when a count exist and it's not a rescheduling workflow */
count: recurringCount && !rescheduleUid ? recurringCount : undefined, count: recurringCount && !rescheduleUid ? recurringCount : undefined,
ethSignature, ...(ethSignature ? { ethSignature } : {}),
}, },
}; };
@@ -114,29 +114,29 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>} {!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
</div> </div>
) : ( ) : (
<Link href={bookingUrl} prefetch={false}> <Link
<a href={bookingUrl}
className={classNames( prefetch={false}
"text-primary-500 hover:border-gray-900 hover:bg-gray-50", className={classNames(
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent dark:text-neutral-200", "text-primary-500 hover:border-gray-900 hover:bg-gray-50",
brand === "#fff" || brand === "#ffffff" ? "" : "" "dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
)} brand === "#fff" || brand === "#ffffff" ? "" : ""
data-testid="time"> )}
{dayjs(slot.time).tz(timeZone()).format(timeFormat)} data-testid="time">
{!!seatsPerTimeSlot && ( {dayjs(slot.time).tz(timeZone()).format(timeFormat)}
<p {!!seatsPerTimeSlot && (
className={`${ <p
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8 className={`${
? "text-rose-600" slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33 ? "text-rose-600"
? "text-yellow-500" : slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
: "text-emerald-400" ? "text-yellow-500"
} text-sm`}> : "text-emerald-400"
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "} } text-sm`}>
{seatsPerTimeSlot} {t("seats_available")} {slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
</p> {seatsPerTimeSlot} {t("seats_available")}
)} </p>
</a> )}
</Link> </Link>
)} )}
</div> </div>
@@ -76,7 +76,7 @@ const BookingDescription: FC<Props> = (props) => {
profile={profile} profile={profile}
users={eventType.users} users={eventType.users}
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN} showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
size={10} size="sm"
truncateAfter={3} truncateAfter={3}
/> />
<h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300"> <h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
@@ -220,7 +220,8 @@ function BookingListItem(booking: BookingItemProps) {
}, },
}); });
}; };
const showRecordingsButtons = booking.location === "integrations:daily" && isPast && isConfirmed; const showRecordingsButtons =
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
return ( return (
<> <>
<RescheduleDialog <RescheduleDialog
@@ -273,7 +274,7 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<tr className="group flex flex-col hover:bg-neutral-50 sm:flex-row"> <tr className="group flex flex-col hover:bg-gray-50 sm:flex-row">
<td <td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]" className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}> onClick={onClickTableData}>
@@ -356,7 +357,7 @@ function BookingListItem(booking: BookingItemProps) {
<div <div
title={booking.title} title={booking.title}
className={classNames( className={classNames(
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-neutral-900 md:max-w-full", "max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-gray-900 md:max-w-full",
isCancelled ? "line-through" : "" isCancelled ? "line-through" : ""
)}> )}>
{booking.title} {booking.title}
@@ -55,7 +55,7 @@ export default function CancelBooking(props: Props) {
placeholder={t("cancellation_reason_placeholder")} placeholder={t("cancellation_reason_placeholder")}
value={cancellationReason} value={cancellationReason}
onChange={(e) => setCancellationReason(e.target.value)} onChange={(e) => setCancellationReason(e.target.value)}
className="mt-2 mb-4 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white " className="dark:bg-darkgray-100 dark:border-darkgray-400 mt-2 mb-4 w-full dark:text-white "
rows={3} rows={3}
/> />
<div className="flex flex-col-reverse rtl:space-x-reverse "> <div className="flex flex-col-reverse rtl:space-x-reverse ">
@@ -4,7 +4,7 @@ import { SkeletonText } from "@calcom/ui";
function SkeletonLoader() { function SkeletonLoader() {
return ( return (
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden"> <ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
<SkeletonItem /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
+2 -4
View File
@@ -1,6 +1,5 @@
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import { AvatarGroup, AvatarGroupProps } from "@calcom/ui";
import AvatarGroup, { AvatarGroupProps } from "@components/ui/AvatarGroup";
export const UserAvatars = ({ export const UserAvatars = ({
profile, profile,
@@ -14,7 +13,6 @@ export const UserAvatars = ({
const showMembers = !users.find((user) => user.name === profile.name) && props.showMembers; const showMembers = !users.find((user) => user.name === profile.name) && props.showMembers;
return ( return (
<AvatarGroup <AvatarGroup
border="border-2 dark:border-darkgray-100 border-white"
items={ items={
[ [
{ {
@@ -38,7 +36,7 @@ export const UserAvatars = ({
href?: string; href?: string;
}[] }[]
} }
size={props.size} size="sm"
truncateAfter={props.truncateAfter} truncateAfter={props.truncateAfter}
/> />
); );
@@ -492,7 +492,7 @@ const BookingPage = ({
<div <div
className={classNames( className={classNames(
"main overflow-hidden", "main overflow-hidden",
isBackgroundTransparent ? "" : "dark:border-1 dark:bg-darkgray-100 bg-white", isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border",
"dark:border-darkgray-300 rounded-md sm:border" "dark:border-darkgray-300 rounded-md sm:border"
)}> )}>
<div className="sm:flex"> <div className="sm:flex">
@@ -954,19 +954,14 @@ const BookingPage = ({
<Button <Button
type="button" type="button"
color="minimal" color="minimal"
size="icon" variant="icon"
tooltip={t("additional_guests")} tooltip={t("additional_guests")}
StartIcon={Icon.FiUserPlus} StartIcon={Icon.FiUserPlus}
onClick={() => setGuestToggle(!guestToggle)} onClick={() => setGuestToggle(!guestToggle)}
className="mr-auto" className="mr-auto"
/> />
)} )}
<Button <Button color="minimal" type="button" onClick={() => router.back()}>
color="minimal"
type="button"
onClick={() => router.back()}
// We override this for this component only for now - as we don't support darkmode everywhere in the app
className="dark:hover:bg-darkgray-200 dark:border-none dark:text-white">
{t("cancel")} {t("cancel")}
</Button> </Button>
<Button <Button
@@ -212,7 +212,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
return ( return (
<Dialog open={isOpenDialog}> <Dialog open={isOpenDialog}>
<DialogContent> <DialogContent disableOverflow>
<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" /> <Icon.FiMapPin className="text-primary-600 h-6 w-6" />
@@ -109,7 +109,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<div className="min-w-4 mb-2"> <div className="min-w-4 mb-2">
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-neutral-700"> <label htmlFor="availability" className="mt-0 flex text-sm font-medium text-gray-700">
{t("availability")} {t("availability")}
</label> </label>
</div> </div>
@@ -183,7 +183,7 @@ function RadioInputHandler({
{...register(`options.${index}.label` as const, { required: true })} {...register(`options.${index}.label` as const, { required: true })}
addOnSuffix={ addOnSuffix={
<Button <Button
size="icon" variant="icon"
color="minimal" color="minimal"
StartIcon={Icon.FiX} StartIcon={Icon.FiX}
onClick={() => { onClick={() => {
@@ -93,10 +93,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex justify-between"> <div className="flex justify-between">
<Label>{t("add_to_calendar")}</Label> <Label>{t("add_to_calendar")}</Label>
<Link href="/apps/categories/calendar"> <Link
<a target="_blank" className="text-sm text-gray-600 hover:text-gray-900"> href="/apps/categories/calendar"
{t("add_another_calendar")} target="_blank"
</a> className="text-sm text-gray-600 hover:text-gray-900">
{t("add_another_calendar")}
</Link> </Link>
</div> </div>
<div className="-mt-1 w-full"> <div className="-mt-1 w-full">
@@ -128,7 +129,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Button <Button
type="button" type="button"
StartIcon={Icon.FiEdit} StartIcon={Icon.FiEdit}
size="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"
onClick={() => setShowEventNameTip((old) => !old)} onClick={() => setShowEventNameTip((old) => !old)}
@@ -266,12 +267,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
/> />
{/*TODO: Extract it out into a component when used more than once*/} {/*TODO: Extract it out into a component when used more than once*/}
{!IS_SELF_HOSTED && ( {!IS_SELF_HOSTED && (
<Link href="https://cal.com/pricing"> <Link href="https://cal.com/pricing" target="_blank">
<a target="_blank"> <Badge variant="orange" className="ml-2">
<Badge variant="orange" className="ml-2"> Platform Only
Platform Only </Badge>
</Badge>
</a>
</Link> </Link>
)} )}
</div> </div>
+7 -33
View File
@@ -1,43 +1,17 @@
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext"; import { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated"; import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
import { EventTypeAppCardComponentProps } from "@calcom/app-store/types"; 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 { RouterOutputs, trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, EmptyScreen, ErrorBoundary, Icon } from "@calcom/ui"; import { Button, EmptyScreen, Icon } from "@calcom/ui";
type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] & export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
EventTypeAppCardComponentProps["eventType"]; EventTypeAppCardComponentProps["eventType"];
function AppCardWrapper({
app,
eventType,
getAppData,
setAppData,
}: {
app: RouterOutputs["viewer"]["apps"][number];
eventType: EventType;
getAppData: GetAppData;
setAppData: SetAppData;
}) {
const dirName = app.slug === "stripe" ? "stripepayment" : app.slug;
const Component = EventTypeAddonMap[dirName as keyof typeof EventTypeAddonMap];
if (!Component) {
throw new Error('No component found for "' + dirName + '"');
}
return (
<ErrorBoundary message={`There is some problem with ${app.name} App`}>
<EventTypeAppContext.Provider value={[getAppData, setAppData]}>
<Component key={app.slug} app={app} eventType={eventType} />
</EventTypeAppContext.Provider>
</ErrorBoundary>
);
}
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
const { t } = useLocale(); const { t } = useLocale();
const { data: eventTypeApps, isLoading } = trpc.viewer.apps.useQuery({ const { data: eventTypeApps, isLoading } = trpc.viewer.apps.useQuery({
@@ -97,7 +71,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
/> />
) : null} ) : null}
{installedApps?.map((app) => ( {installedApps?.map((app) => (
<AppCardWrapper <EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)} getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)} setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug} key={app.slug}
@@ -113,7 +87,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
) : null} ) : null}
<div className="before:border-0"> <div className="before:border-0">
{notInstalledApps?.map((app) => ( {notInstalledApps?.map((app) => (
<AppCardWrapper <EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)} getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)} setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug} key={app.slug}
+192 -208
View File
@@ -1,8 +1,8 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group"; import * as RadioGroup from "@radix-ui/react-radio-group";
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useRef, useState } from "react"; import React, { useEffect, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form"; import { Controller, useFormContext, UseFormRegisterReturn, useWatch } from "react-hook-form";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import convertToNewDurationType, { DurationType } from "@calcom/lib/convertToNewDurationType"; import convertToNewDurationType, { DurationType } from "@calcom/lib/convertToNewDurationType";
@@ -12,13 +12,95 @@ 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, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
const MinimumBookingNoticeInput = React.forwardRef<
HTMLInputElement,
Omit<UseFormRegisterReturn<"minimumBookingNotice">, "ref">
>(function MinimumBookingNoticeInput({ ...passThroughProps }, ref) {
const { t } = useLocale();
const { setValue, getValues } = useFormContext<FormValues>();
const durationTypeOptions: {
value: DurationType;
label: string;
}[] = [
{
label: t("minutes"),
value: "minutes",
},
{
label: t("hours"),
value: "hours",
},
{
label: t("days"),
value: "days",
},
];
const [minimumBookingNoticeDisplayValues, setMinimumBookingNoticeDisplayValues] = useState<{
type: DurationType;
value: number;
}>({
type: findDurationType(getValues(passThroughProps.name)),
value: convertToNewDurationType(
"minutes",
findDurationType(getValues(passThroughProps.name)),
getValues(passThroughProps.name)
),
});
// keep hidden field in sync with minimumBookingNoticeDisplayValues
useEffect(() => {
setValue(
passThroughProps.name,
convertToNewDurationType(
minimumBookingNoticeDisplayValues.type,
"minutes",
minimumBookingNoticeDisplayValues.value
)
);
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
return (
<div className="flex items-end justify-end">
<div className="w-1/2 md:w-full">
<InputField
required
defaultValue={minimumBookingNoticeDisplayValues.value}
onChange={(e) =>
setMinimumBookingNoticeDisplayValues({
...minimumBookingNoticeDisplayValues,
value: parseInt(e.target.value || "0", 10),
})
}
label={t("minimum_booking_notice")}
type="number"
placeholder="0"
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
/>
<input type="hidden" ref={ref} {...passThroughProps} />
</div>
<Select
isSearchable={false}
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
defaultValue={durationTypeOptions.find(
(option) => option.value === minimumBookingNoticeDisplayValues.type
)}
onChange={(input) => {
if (input) {
setMinimumBookingNoticeDisplayValues({
...minimumBookingNoticeDisplayValues,
type: input.value,
});
}
}}
options={durationTypeOptions}
/>
</div>
);
});
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => { export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
const { t } = useLocale(); const { t } = useLocale();
const formMethods = useFormContext<FormValues>(); const formMethods = useFormContext<FormValues>();
const minimumBookingNoticeType = useRef<DurationType>(findDurationType(eventType.minimumBookingNotice));
const prevBookingNoticeType = useRef<DurationType>(minimumBookingNoticeType.current);
const minimumBookingNoticeInDurationTypeFormValue = formMethods.watch("minimumBookingNoticeInDurationType");
const PERIOD_TYPES = [ const PERIOD_TYPES = [
{ {
@@ -49,212 +131,115 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
defaultValue: periodType?.type, defaultValue: periodType?.type,
}); });
const onMinimumNoticeDurationTypeChange = useMemo(
() => (durationType?: DurationType) => {
if (typeof durationType === "undefined") return;
// Store current selected type in ref to use in previous run for comparrison.
minimumBookingNoticeType.current = durationType;
// Uses the previous selected type and converts the value to the new type.
// Eg day was selected before, user selects hours now, so we multiple * 24 and round up.
const minimumBookingNoticeInDurationType = convertToNewDurationType(
prevBookingNoticeType.current,
durationType,
Number(minimumBookingNoticeInDurationTypeFormValue)
);
// Uses the round up value in the new type to calculate the value in minutes.
// We should NOT use minimumBookingNoticeInDurationTypeFormValue here, since that
// would break in the case that the user enters 90 minutes, changes the type to
// hours (that will show 2 hours), but the value in minutes would then remain 90,
// which is something else than the user things they save.
const minimumBookingNoticeInMinutes = convertToNewDurationType(
durationType,
"minutes",
Number(minimumBookingNoticeInDurationType)
);
// Updates both form values as well as stores the current type as previous type in ref.
formMethods.setValue("minimumBookingNotice", minimumBookingNoticeInMinutes);
formMethods.setValue("minimumBookingNoticeInDurationType", minimumBookingNoticeInDurationType);
prevBookingNoticeType.current = durationType;
},
[formMethods, minimumBookingNoticeInDurationTypeFormValue]
);
/**
* When the user inputs a new value for minimumBookingNoticeInDurationType,
* we calculate the value in minutes, and update this hidden field as well.
*/
const onMinimumNoticeChange = useMemo(
() => (duration: string) => {
const minimumBookingNoticeInMinutes = convertToNewDurationType(
minimumBookingNoticeType.current,
"minutes",
Number(duration)
);
formMethods.setValue("minimumBookingNotice", minimumBookingNoticeInMinutes);
},
[formMethods]
);
return ( return (
<div> <div className="space-y-8">
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4"> <div className="space-y-4 lg:space-y-8">
<div className="w-full"> <div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label> <div className="w-full">
<Controller <Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
name="beforeBufferTime" <Controller
control={formMethods.control} name="beforeBufferTime"
defaultValue={eventType.beforeEventBuffer || 0} control={formMethods.control}
render={({ field: { onChange, value } }) => { defaultValue={eventType.beforeEventBuffer || 0}
const beforeBufferOptions = [ render={({ field: { onChange, value } }) => {
{ const beforeBufferOptions = [
label: t("event_buffer_default"), {
value: 0, label: t("event_buffer_default"),
}, value: 0,
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({ },
label: minutes + " " + t("minutes"), ...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
value: minutes, label: minutes + " " + t("minutes"),
})), value: minutes,
]; })),
return ( ];
<Select return (
isSearchable={false}
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
}
options={beforeBufferOptions}
/>
);
}}
/>
</div>
<div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
defaultValue={eventType.afterEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
label: t("event_buffer_default"),
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
}
options={afterBufferOptions}
/>
);
}}
/>
</div>
</div>
<div className="flex flex-col space-y-4 pt-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="flex w-full items-end justify-end">
<Controller
name="minimumBookingNotice"
control={formMethods.control}
render={() => {
const durationTypeOptions = [
{
label: t("minutes"),
value: "minutes",
},
{
label: t("hours"),
value: "hours",
},
{
label: t("days"),
value: "days",
},
];
return (
<>
<div className="w-1/2 md:w-3/4">
<InputField
required
label={t("minimum_booking_notice")}
type="number"
placeholder="120"
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
{...formMethods.register("minimumBookingNoticeInDurationType", {
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
onMinimumNoticeChange(event.target.value),
})}
/>
<input type="hidden" {...formMethods.register("minimumBookingNotice")} />
</div>
<Select <Select
isSearchable={false} isSearchable={false}
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]" onChange={(val) => {
defaultValue={durationTypeOptions.find( if (val) onChange(val.value);
(option) => option.value === minimumBookingNoticeType.current }}
)} defaultValue={
onChange={(input) => onMinimumNoticeDurationTypeChange(input?.value as DurationType)} beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
options={durationTypeOptions} }
options={beforeBufferOptions}
/> />
</> );
); }}
}} />
/> </div>
<div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
defaultValue={eventType.afterEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
label: t("event_buffer_default"),
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
}
options={afterBufferOptions}
/>
);
}}
/>
</div>
</div> </div>
<div className="w-full"> <div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<Label htmlFor="slotInterval">{t("slot_interval")} </Label> <div className="w-full">
<Controller <Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
name="slotInterval" <MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
control={formMethods.control} </div>
render={() => { <div className="w-full">
const slotIntervalOptions = [ <Label htmlFor="slotInterval">{t("slot_interval")} </Label>
{ <Controller
label: t("slot_interval_default"), name="slotInterval"
value: -1, control={formMethods.control}
}, render={() => {
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({ const slotIntervalOptions = [
label: minutes + " " + t("minutes"), {
value: minutes, label: t("slot_interval_default"),
})), value: -1,
]; },
return ( ...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
<Select label: minutes + " " + t("minutes"),
isSearchable={false} value: minutes,
onChange={(val) => { })),
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null); ];
}} return (
defaultValue={ <Select
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) || isSearchable={false}
slotIntervalOptions[0] onChange={(val) => {
} formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
options={slotIntervalOptions} }}
/> defaultValue={
); slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
}} slotIntervalOptions[0]
/> }
options={slotIntervalOptions}
/>
);
}}
/>
</div>
</div> </div>
</div> </div>
<hr />
<hr className="my-8" />
<Controller <Controller
name="bookingLimits" name="bookingLimits"
control={formMethods.control} control={formMethods.control}
@@ -276,8 +261,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</SettingsToggle> </SettingsToggle>
)} )}
/> />
<hr />
<hr className="my-8" />
<Controller <Controller
name="periodType" name="periodType"
control={formMethods.control} control={formMethods.control}
@@ -446,7 +430,7 @@ const BookingLimits = () => {
}} }}
/> />
<Button <Button
size="icon" variant="icon"
StartIcon={Icon.FiTrash} StartIcon={Icon.FiTrash}
color="destructive" color="destructive"
onClick={() => { onClick={() => {
@@ -1,16 +1,18 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
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 type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useState } from "react"; import { useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form"; import { Controller, useForm, useFormContext } from "react-hook-form";
import { MultiValue } from "react-select"; import { MultiValue } from "react-select";
import { z } from "zod"; import { z } from "zod";
import { EventLocationType, getEventLocationType } 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, Skeleton, TextField, SettingsToggle } from "@calcom/ui"; import { Button, Icon, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
import { slugify } from "@lib/slugify"; import { slugify } from "@lib/slugify";
@@ -160,7 +162,7 @@ export const EventSetupTab = (
return null; return null;
} }
return ( return (
<li key={location.type} className="mb-2 rounded-md border border-neutral-300 py-1.5 px-2"> <li key={location.type} className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
<div className="flex max-w-full justify-between"> <div className="flex max-w-full justify-between">
<div key={index} className="flex flex-grow items-center"> <div key={index} className="flex flex-grow items-center">
<img <img
@@ -195,6 +197,23 @@ export const EventSetupTab = (
</li> </li>
); );
})} })}
{validLocations.some((location) => location.type === MeetLocationType) && (
<div className="flex text-sm text-gray-600">
<Icon.FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
<Trans i18nKey="event_type_requres_google_cal">
<p>
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
Change it{" "}
<Link
href={`${CAL_URL}/event-types/${eventType.id}?tabName=advanced`}
className="underline">
here.
</Link>{" "}
We will fall back to Cal video if you do not change it.
</p>
</Trans>
</div>
)}
{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={Icon.FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
@@ -326,6 +345,7 @@ export const EventSetupTab = (
<Skeleton as={Label} loadingClassName="w-16"> <Skeleton as={Label} loadingClassName="w-16">
{t("location")} {t("location")}
</Skeleton> </Skeleton>
<Controller <Controller
name="locations" name="locations"
control={formMethods.control} control={formMethods.control}
+135 -36
View File
@@ -1,9 +1,12 @@
import { SchedulingType } from "@prisma/client/"; import { SchedulingType } from "@prisma/client";
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo } from "react"; import { ComponentProps } from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext, useWatch, Control } from "react-hook-form";
import { Options } from "react-select";
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect"; import CheckedTeamSelect, {
CheckedSelectOption,
} from "@calcom/features/eventtypes/components/CheckedTeamSelect";
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 { Label, Select } from "@calcom/ui"; import { Label, Select } from "@calcom/ui";
@@ -22,14 +25,66 @@ const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
email, email,
}); });
export const EventTeamTab = ({ const FixedHosts = ({
eventType, control,
team, labelText,
teamMembers, placeholder,
}: Pick<EventTypeSetupProps, "eventType" | "teamMembers" | "team">) => { options = [],
...rest
}: {
control: Control<FormValues>;
labelText: string;
placeholder: string;
options?: Options<CheckedSelectOption>;
} & Partial<ComponentProps<typeof CheckedTeamSelect>>) => {
return (
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
<div>
<Label>{labelText}</Label>
<Controller
name="hostsFixed"
control={control}
render={({ field: { onChange, value } }) => {
return (
<CheckedTeamSelect
isDisabled={false}
onChange={(options) => {
onChange(
options.map((option) => ({
isFixed: true,
userId: parseInt(option.value, 10),
}))
);
}}
value={value
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={options}
placeholder={placeholder}
{...rest}
/>
);
}}
/>
</div>
</div>
);
};
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
const formMethods = useFormContext<FormValues>(); const formMethods = useFormContext<FormValues>();
const { t } = useLocale(); const { t } = useLocale();
const schedulingType = useWatch({
control: formMethods.control,
name: "schedulingType",
});
const schedulingTypeOptions: { const schedulingTypeOptions: {
value: SchedulingType; value: SchedulingType;
label: string; label: string;
@@ -47,14 +102,13 @@ export const EventTeamTab = ({
}, },
]; ];
const teamMembersToValues = useMemo(() => { const teamMembersOptions = teamMembers.map(mapUserToValue);
return teamMembers.map(mapUserToValue);
}, [teamMembers]);
return ( return (
<div> <div>
{team && ( {team && (
<div className="space-y-3"> <div className="space-y-5">
<div className="flex flex-col pb-8"> <div className="flex flex-col">
<Label>{t("scheduling_type")}</Label> <Label>{t("scheduling_type")}</Label>
<Controller <Controller
name="schedulingType" name="schedulingType"
@@ -72,30 +126,75 @@ export const EventTeamTab = ({
/> />
</div> </div>
<div className="flex flex-col"> {schedulingType === SchedulingType.COLLECTIVE && (
<Label>{t("team")}</Label> <FixedHosts
<Controller options={teamMembersOptions}
name="users" placeholder={t("add_attendees")}
labelText={t("team")}
control={formMethods.control} control={formMethods.control}
defaultValue={eventType.users.map((user) => user.id.toString())}
render={({ field: { onChange, value } }) => (
<CheckedTeamSelect
isDisabled={false}
onChange={(options) => onChange(options.map((user) => user.value))}
value={value
.map(
(userId) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
teamMembers.map(mapUserToValue).find((member) => member.value === userId)!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={teamMembersToValues}
placeholder={t("add_attendees")}
/>
)}
/> />
</div> )}
{schedulingType === SchedulingType.ROUND_ROBIN && (
<>
<FixedHosts
options={teamMembersOptions}
isOptionDisabled={(option) =>
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
}
placeholder={t("add_fixed_hosts")}
labelText={t("fixed_hosts")}
control={formMethods.control}
/>
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
<div>
<Label>{t("round_robin_hosts")}</Label>
<Controller
name="hosts"
control={formMethods.control}
render={({ field: { onChange, value } }) => (
<CheckedTeamSelect
isDisabled={false}
onChange={(options) =>
onChange(
options.map((option) => ({
isFixed: false,
userId: parseInt(option.value, 10),
}))
)
}
value={value
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
teamMembers
.map(mapUserToValue)
.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={teamMembers.map(mapUserToValue)}
isOptionDisabled={(option) =>
!!formMethods
.getValues("hostsFixed")
.find((host) => host.userId.toString() === option.value)
}
placeholder={t("add_attendees")}
/>
)}
/>
</div>
{/*<TextField
required
type="number"
label={t("minimum_round_robin_hosts_count")}
defaultValue={1}
{...formMethods.register("minimumHostCount")}
addOnSuffix={<>{t("hosts")}</>}
/>*/}
</div>
</>
)}
</div> </div>
)} )}
</div> </div>
@@ -1,10 +1,11 @@
import { TFunction } from "next-i18next"; import { TFunction } from "next-i18next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useState } from "react"; import { useMemo, useState, Suspense } from "react";
import { UseFormReturn } from "react-hook-form"; import { UseFormReturn } from "react-hook-form";
import { TbWebhook } from "react-icons/tb"; import { TbWebhook } from "react-icons/tb";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
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";
@@ -15,15 +16,15 @@ import {
ButtonGroup, ButtonGroup,
ConfirmationDialogContent, ConfirmationDialogContent,
Dialog, Dialog,
Divider, DropdownMenuSeparator,
Dropdown, Dropdown,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownItem,
DropdownMenuTrigger, DropdownMenuTrigger,
HorizontalTabs, HorizontalTabs,
Icon, Icon,
Label, Label,
Shell,
showToast, showToast,
Skeleton, Skeleton,
Switch, Switch,
@@ -32,7 +33,6 @@ import {
VerticalTabs, VerticalTabs,
} from "@calcom/ui"; } from "@calcom/ui";
import { ClientSuspense } from "@components/ClientSuspense";
import { EmbedButton, EmbedDialog } from "@components/Embed"; import { EmbedButton, EmbedDialog } from "@components/Embed";
type Props = { type Props = {
@@ -56,12 +56,15 @@ function getNavigation(props: {
installedAppsNumber: number; installedAppsNumber: number;
}) { }) {
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props; const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
const duration =
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
return [ return [
{ {
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: Icon.FiLink,
info: `${eventType.length} Mins`, // TODO: Get this from props info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
}, },
{ {
name: "availability", name: "availability",
@@ -152,7 +155,7 @@ function EventTypeSingleLayout({
// If there is a team put this navigation item within the tabs // If there is a team put this navigation item within the tabs
if (team) { if (team) {
navigation.splice(2, 0, { navigation.splice(2, 0, {
name: "scheduling_type", name: "assignment",
href: `/event-types/${eventType.id}?tabName=team`, href: `/event-types/${eventType.id}?tabName=team`,
icon: Icon.FiUsers, icon: Icon.FiUsers,
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin", info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
@@ -205,7 +208,7 @@ function EventTypeSingleLayout({
<Button <Button
color="secondary" color="secondary"
target="_blank" target="_blank"
size="icon" variant="icon"
href={permalink} href={permalink}
rel="noreferrer" rel="noreferrer"
StartIcon={Icon.FiExternalLink} StartIcon={Icon.FiExternalLink}
@@ -214,7 +217,7 @@ function EventTypeSingleLayout({
<Button <Button
color="secondary" color="secondary"
size="icon" variant="icon"
StartIcon={Icon.FiLink} StartIcon={Icon.FiLink}
tooltip={t("copy_link")} tooltip={t("copy_link")}
onClick={() => { onClick={() => {
@@ -226,12 +229,12 @@ function EventTypeSingleLayout({
embedUrl={encodeURIComponent(embedLink)} embedUrl={encodeURIComponent(embedLink)}
StartIcon={Icon.FiCode} StartIcon={Icon.FiCode}
color="secondary" color="secondary"
size="icon" variant="icon"
tooltip={t("embed")} tooltip={t("embed")}
/> />
<Button <Button
color="secondary" color="secondary"
size="icon" variant="icon"
StartIcon={Icon.FiTrash} StartIcon={Icon.FiTrash}
tooltip={t("delete")} tooltip={t("delete")}
disabled={!hasPermsToDelete} disabled={!hasPermsToDelete}
@@ -247,32 +250,36 @@ function EventTypeSingleLayout({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem className="focus:ring-gray-100"> <DropdownMenuItem className="focus:ring-gray-100">
<Button <DropdownItem
color="minimal"
StartIcon={Icon.FiExternalLink}
target="_blank" target="_blank"
type="button"
StartIcon={Icon.FiExternalLink}
href={permalink} href={permalink}
rel="noreferrer" rel="noreferrer">
className="min-w-full">
{t("preview")} {t("preview")}
</Button> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100"> <DropdownMenuItem className="focus:ring-gray-100">
<Button color="minimal" StartIcon={Icon.FiLink}> <DropdownItem
type="button"
StartIcon={Icon.FiLink}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
}}>
{t("copy_link")} {t("copy_link")}
</Button> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100"> <DropdownMenuItem className="focus:ring-gray-100">
<Button <DropdownItem
color="minimal" type="button"
StartIcon={Icon.FiTrash} StartIcon={Icon.FiTrash}
disabled={!hasPermsToDelete} disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)} onClick={() => setDeleteDialogOpen(true)}>
className="min-w-full">
{t("delete")} {t("delete")}
</Button> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<Divider /> <DropdownMenuSeparator className="block sm:hidden" />
<div className="flex items-center rounded-md py-1.5 px-4 sm:hidden sm:hover:bg-gray-100"> <div className="flex items-center rounded-md py-1.5 px-4 sm:hidden sm:hover:bg-gray-100">
<Skeleton <Skeleton
as={Label} as={Label}
@@ -301,7 +308,7 @@ function EventTypeSingleLayout({
</Button> </Button>
</div> </div>
}> }>
<ClientSuspense fallback={<Icon.FiLoader />}> <Suspense fallback={<Icon.FiLoader />}>
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8"> <div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
<div className="hidden xl:block"> <div className="hidden xl:block">
<VerticalTabs <VerticalTabs
@@ -317,14 +324,14 @@ function EventTypeSingleLayout({
<div className="w-full ltr:mr-2 rtl:ml-2"> <div className="w-full ltr:mr-2 rtl:ml-2">
<div <div
className={classNames( className={classNames(
"mt-4 rounded-md border-neutral-200 bg-white sm:mx-0 xl:mt-0", "mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6" disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6"
)}> )}>
{children} {children}
</div> </div>
</div> </div>
</div> </div>
</ClientSuspense> </Suspense>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<ConfirmationDialogContent <ConfirmationDialogContent
isLoading={deleteMutation.isLoading} isLoading={deleteMutation.isLoading}
@@ -57,7 +57,7 @@ export default function RecurringEventController({
{recurringEventState && ( {recurringEventState && (
<div data-testid="recurring-event-collapsible" className="text-sm"> <div data-testid="recurring-event-collapsible" className="text-sm">
<div className="flex items-center"> <div className="flex items-center">
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p> <p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
<input <input
type="number" type="number"
min="1" min="1"
@@ -89,7 +89,7 @@ export default function RecurringEventController({
/> />
</div> </div>
<div className="mt-4 flex items-center"> <div className="mt-4 flex items-center">
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p> <p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
<input <input
type="number" type="number"
min="1" min="1"
@@ -105,7 +105,7 @@ export default function RecurringEventController({
setRecurringEventState(newVal); setRecurringEventState(newVal);
}} }}
/> />
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2"> <p className="text-gray-900 ltr:mr-2 rtl:ml-2">
{t("events", { {t("events", {
count: recurringEventState.count, count: recurringEventState.count,
})} })}
@@ -10,7 +10,7 @@ function SkeletonLoader() {
<SkeletonText className="h-4 w-24" /> <SkeletonText className="h-4 w-24" />
</div> </div>
</div> </div>
<ul className="divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden"> <ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
@@ -16,7 +16,7 @@ const CreateEventsOnCalendarSelect = (props: ICreateEventsOnCalendarSelectProps)
<> <>
<div className="mt-6 flex flex-row"> <div className="mt-6 flex flex-row">
<div className="w-full"> <div className="w-full">
<label htmlFor="createEventsOn" className="flex text-sm font-medium text-neutral-700"> <label htmlFor="createEventsOn" className="flex text-sm font-medium text-gray-700">
{t("create_events_on")} {t("create_events_on")}
</label> </label>
<div className="mt-2"> <div className="mt-2">
@@ -113,7 +113,7 @@ const UserProfile = (props: IUserProfileProps) => {
name="avatar" name="avatar"
id="avatar" id="avatar"
placeholder="URL" placeholder="URL"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm focus:border-neutral-800 focus:outline-none focus:ring-neutral-800" className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm focus:border-gray-800 focus:outline-none focus:ring-gray-800"
defaultValue={imageSrc} defaultValue={imageSrc}
/> />
<div className="flex items-center px-4"> <div className="flex items-center px-4">
@@ -146,7 +146,7 @@ const UserProfile = (props: IUserProfileProps) => {
ref={bioRef} ref={bioRef}
name="bio" name="bio"
id="bio" id="bio"
className="mt-1 block h-[60px] w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm" 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} defaultValue={user?.bio || undefined}
onChange={(event) => { onChange={(event) => {
setValue("bio", event.target.value); setValue("bio", event.target.value);
@@ -19,7 +19,8 @@ interface IUserSettingsProps {
const UserSettings = (props: IUserSettingsProps) => { const UserSettings = (props: IUserSettingsProps) => {
const { user, nextStep } = props; const { user, nextStep } = props;
const { t } = useLocale(); const { t } = useLocale();
const [selectedTimeZone, setSelectedTimeZone] = useState(user.timeZone ?? dayjs.tz.guess()); const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
const { const {
register, register,
handleSubmit, handleSubmit,
+2 -2
View File
@@ -22,8 +22,8 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"} imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
/> />
<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-neutral-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-neutral-500 dark:text-white"> <p 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 })} {member.bio || t("user_from_team", { user: member.name, team: teamName })}
</p> </p>
</section> </section>
+3 -3
View File
@@ -24,7 +24,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
)} )}
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}> <div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
{props.heading && <h2 className="font-cal text-center text-3xl text-neutral-900">{props.heading}</h2>} {props.heading && <h2 className="font-cal text-center text-3xl text-gray-900">{props.heading}</h2>}
</div> </div>
{props.loading && ( {props.loading && (
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50"> <div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
@@ -32,10 +32,10 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
</div> </div>
)} )}
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md"> <div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="border-1 mx-2 rounded-md 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-neutral-600"> <div className="mt-8 text-center text-sm text-gray-600">
{props.footerText} {props.footerText}
<Credits /> <Credits />
</div> </div>
+1 -7
View File
@@ -35,13 +35,7 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
return ( return (
<li key={idx} className="-ltr:mr-2 inline-block rtl:ml-2"> <li key={idx} className="-ltr:mr-2 inline-block rtl:ml-2">
{item.href ? ( {item.href ? <Link href={item.href}>{avatar}</Link> : avatar}
<Link href={item.href}>
<a>{avatar}</a>
</Link>
) : (
avatar
)}
</li> </li>
); );
} }
+1 -1
View File
@@ -13,7 +13,7 @@ export default function LinkIconButton(props: LinkIconButtonProps) {
type="button" type="button"
{...props} {...props}
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900"> className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900">
<props.Icon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" /> <props.Icon className="h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" />
{props.children} {props.children}
</button> </button>
</div> </div>
-22
View File
@@ -1,22 +0,0 @@
import Link from "next/link";
interface ILinkTextProps {
href: string;
children: React.ReactNode;
classNameChildren?: string;
target?: string;
}
/**
* This component had to be made in order to make i18n work with next/link
* @see https://github.com/i18next/react-i18next/issues/1090#issuecomment-615426145
**/
export const LinkText = (props: ILinkTextProps) => {
const { target, href, children, classNameChildren, ...moreProps } = props;
return (
<Link href={href || ""} {...moreProps}>
<a target={target || ""} className={classNameChildren}>
{children}
</a>
</Link>
);
};
+15 -20
View File
@@ -9,26 +9,21 @@ const PoweredByCal = () => {
const isEmbed = useIsEmbed(); const isEmbed = useIsEmbed();
return ( return (
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}> <div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
<Link href={POWERED_BY_URL}> <Link
<a target="_blank" className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white"> href={POWERED_BY_URL}
{t("powered_by")}{" "} target="_blank"
{ className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
// eslint-disable-next-line @next/next/no-img-element {t("powered_by")}{" "}
<img <img
className="relative -mt-px inline h-[10px] w-auto dark:hidden" className="relative -mt-px inline h-[10px] w-auto dark:hidden"
src="/cal-logo-word.svg" src="/cal-logo-word.svg"
alt="Cal.com Logo" alt="Cal.com Logo"
/> />
} <img
{ className="relativ -mt-px hidden h-[10px] w-auto dark:inline"
// eslint-disable-next-line @next/next/no-img-element src="/cal-logo-word-dark.svg"
<img alt="Cal.com Logo"
className="relativ -mt-px hidden h-[10px] w-auto dark:inline" />
src="/cal-logo-word-dark.svg"
alt="Cal.com Logo"
/>
}
</a>
</Link> </Link>
</div> </div>
); );
@@ -13,8 +13,8 @@ export default function SettingInputContainer({
<div className="space-y-3"> <div className="space-y-3">
<div className="block sm:flex"> <div className="block sm:flex">
<div className="min-w-48 mb-4 sm:mb-0"> <div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor={htmlFor} className="mt-1 flex text-sm font-medium text-neutral-700"> <label htmlFor={htmlFor} className="mt-1 flex text-sm font-medium text-gray-700">
<Icon className="mt-0.5 h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" /> <Icon className="mt-0.5 h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" />
{label} {label}
</label> </label>
</div> </div>
@@ -187,10 +187,10 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<div className="flex justify-items-center"> <div className="flex justify-items-center">
<Label htmlFor="username">{t("username")}</Label> <Label htmlFor="username">{t("username")}</Label>
</div> </div>
<div className="mt-2 flex rounded-md"> <div className="flex rounded-md">
<span <span
className={classNames( className={classNames(
isInputUsernamePremium ? "border-1 border-orange-400 " : "", isInputUsernamePremium ? "border border-orange-400 " : "",
"hidden h-9 items-center rounded-l-md border border-r-0 border-gray-300 border-r-gray-300 bg-gray-50 px-3 text-sm text-gray-500 md:inline-flex" "hidden h-9 items-center rounded-l-md border border-r-0 border-gray-300 border-r-gray-300 bg-gray-50 px-3 text-sm text-gray-500 md:inline-flex"
)}> )}>
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/ {process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
@@ -207,8 +207,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
className={classNames( className={classNames(
"border-l-1 mb-0 mt-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0", "border-l-1 mb-0 mt-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0",
isInputUsernamePremium isInputUsernamePremium
? "border-1 focus:border-1 border-orange-400 focus:border-orange-400" ? "border border-orange-400 focus:border focus:border-orange-400"
: "border-1 focus:border-2", : "border focus:border",
markAsError markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none" ? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
: "border-l-gray-300", : "border-l-gray-300",
@@ -118,7 +118,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
return ( return (
<div> <div>
<div className="mt-2 flex rounded-md"> <div className="flex rounded-md">
<div className="relative w-full"> <div className="relative w-full">
<TextField <TextField
ref={usernameRef} ref={usernameRef}
@@ -21,7 +21,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
{React.createElement( {React.createElement(
descriptionAsLabel ? "div" : "label", descriptionAsLabel ? "div" : "label",
{ {
className: "flex text-sm font-medium text-neutral-700", className: "flex text-sm font-medium text-gray-700",
...(!descriptionAsLabel ...(!descriptionAsLabel
? { ? {
htmlFor: rest.id, htmlFor: rest.id,
@@ -39,7 +39,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
{ {
className: classNames( className: classNames(
"relative flex items-start", "relative flex items-start",
descriptionAsLabel ? "text-neutral-700" : "text-neutral-900" descriptionAsLabel ? "text-gray-700" : "text-gray-900"
), ),
}, },
<> <>
@@ -41,7 +41,7 @@ export const CheckedSelect = ({
{...props} {...props}
/> />
{value.map((option) => ( {value.map((option) => (
<div key={option.value} className="border-1 border p-2 font-medium"> <div key={option.value} className="border p-2 font-medium">
<Avatar <Avatar
className="inline h-6 w-6 rounded-full ltr:mr-2 rtl:ml-2" className="inline h-6 w-6 rounded-full ltr:mr-2 rtl:ml-2"
imageSrc={option.avatar} imageSrc={option.avatar}
@@ -50,7 +50,7 @@ export const CheckedSelect = ({
{option.label} {option.label}
<Icon.FiX <Icon.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-neutral-500" className="float-right mt-0.5 h-5 w-5 cursor-pointer text-gray-500"
/> />
</div> </div>
))} ))}
+1 -1
View File
@@ -10,7 +10,7 @@ const MinutesField = forwardRef<HTMLInputElement, Props>(({ label, ...rest }, re
<div className="block sm:flex"> <div className="block sm:flex">
{!!label && ( {!!label && (
<div className="min-w-48 mb-4 sm:mb-0"> <div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor={rest.id} className="flex h-full items-center text-sm font-medium text-neutral-700"> <label htmlFor={rest.id} className="flex h-full items-center text-sm font-medium text-gray-700">
{label} {label}
</label> </label>
</div> </div>
@@ -1,79 +0,0 @@
import React, { ReactNode, useState } from "react";
import classNames from "@lib/classNames";
type RadioAreaProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> & {
onChange?: (value: string) => void;
defaultChecked?: boolean;
};
const RadioArea = (props: RadioAreaProps) => {
return (
<label
className={classNames(
"border-1 block border p-4 focus:outline-none focus:ring focus:ring-neutral-500",
props.checked && "border-brand",
props.className
)}>
<input
onChange={(e) => {
if (typeof props.onChange === "function") {
props.onChange(e.target.value);
}
}}
checked={props.checked}
className="float-right ml-3 text-neutral-900 focus:ring-neutral-500"
name={props.name}
value={props.value}
type="radio"
/>
{props.children}
</label>
);
};
type ChildrenProps = {
props: RadioAreaProps;
children?: ReactNode;
};
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
children: ChildrenProps | ChildrenProps[];
name?: string;
onChange?: (value: string) => void;
}
const RadioAreaGroup = ({ children, name, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
const [checkedIdx, setCheckedIdx] = useState<number | null>(null);
const changeHandler = (value: string, idx: number) => {
if (onChange) {
onChange(value);
}
setCheckedIdx(idx);
};
return (
<div {...passThroughProps}>
{(Array.isArray(children) ? children : [children]).map((child, idx: number) => {
if (checkedIdx === null && child.props.defaultChecked) {
setCheckedIdx(idx);
}
return (
<Item
{...child.props}
key={idx}
name={name}
checked={idx === checkedIdx}
onChange={(value: string) => changeHandler(value, idx)}>
{child.props.children}
</Item>
);
})}
</div>
);
};
const Item = RadioArea;
const Group = RadioAreaGroup;
export { RadioArea, RadioAreaGroup, Item, Group };
@@ -1,61 +0,0 @@
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import React from "react";
import { Icon } from "@calcom/ui";
import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
import { RadioArea, RadioAreaGroup } from "@components/ui/form/radio-area/RadioAreaGroup";
interface OptionProps
extends Pick<React.OptionHTMLAttributes<HTMLOptionElement>, "value" | "label" | "className"> {
description?: string;
}
interface RadioAreaSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange"> {
options: OptionProps[]; // allow options to be passed programmatically, like options={}
onChange?: (value: string) => void;
}
export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) {
const { t } = useLocale();
const {
options,
disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway
placeholder = t("select"),
} = props;
const getLabel = (value: string | ReadonlyArray<string> | number | undefined) =>
options.find((option: OptionProps) => option.value === value)?.label;
return (
<Collapsible className={classNames("w-full", props.className)}>
<CollapsibleTrigger
type="button"
disabled={disabled}
className={classNames(
"border-1 focus:ring-primary-500 mb-1 block w-full cursor-pointer rounded-sm border border-gray-300 bg-white p-2 text-left text-sm",
disabled && "cursor-default bg-gray-200 focus:ring-0 "
)}>
{getLabel(props.value) ?? placeholder}
<Icon.FiChevronDown className="float-right h-5 w-5 text-neutral-500" />
</CollapsibleTrigger>
<CollapsibleContent>
<RadioAreaGroup className="space-y-2 text-sm" name={props.name} onChange={props.onChange}>
{options.map((option) => (
<RadioArea
{...option}
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}
defaultChecked={props.value === option.value}>
<strong className="mb-1 block">{option.label}</strong>
<p>{option.description}</p>
</RadioArea>
))}
</RadioAreaGroup>
</CollapsibleContent>
</Collapsible>
);
};
export default Select;
@@ -1,2 +0,0 @@
export * from "./RadioAreaGroup";
export * from "./Select";
+1
View File
@@ -1,3 +1,4 @@
// lets refactor and move this into packages/lib/hooks/
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
const useMediaQuery = (query: string) => { const useMediaQuery = (query: string) => {
+6
View File
@@ -89,6 +89,12 @@ const nextConfig = {
eslint: { eslint: {
ignoreDuringBuilds: !!process.env.CI, ignoreDuringBuilds: !!process.env.CI,
}, },
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
// modularizeImports: {
// "@calcom/ui": {
// transform: "@calcom/ui/components/{{member}}",
// },
// },
images: { images: {
unoptimized: true, unoptimized: true,
}, },
+5 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "2.4.4", "version": "2.5.2",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
@@ -67,6 +67,7 @@
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"dotenv-cli": "^6.0.0", "dotenv-cli": "^6.0.0",
"eslint-config-next": "^13.1.1",
"googleapis": "^84.0.0", "googleapis": "^84.0.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
@@ -80,16 +81,14 @@
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"micro": "^9.4.1", "micro": "^9.4.1",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"next": "^12.3.1", "next": "^13.1.1",
"next-auth": "^4.10.3", "next-auth": "^4.10.3",
"next-axiom": "^0.10.0", "next-axiom": "^0.16.0",
"next-collect": "^0.2.1", "next-collect": "^0.2.1",
"next-i18next": "^11.3.0", "next-i18next": "^11.3.0",
"next-mdx-remote": "^4.1.0",
"next-seo": "^4.26.0", "next-seo": "^4.26.0",
"next-themes": "^0.2.0", "next-themes": "^0.2.0",
"next-transpile-modules": "^9.0.0", "next-transpile-modules": "^10.0.0",
"nock": "^13.2.8",
"nodemailer": "^6.7.8", "nodemailer": "^6.7.8",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
+46 -48
View File
@@ -98,26 +98,26 @@ export default function Custom404() {
<ul role="list" className="divide-y divide-gray-200 border-gray-200"> <ul role="list" className="divide-y divide-gray-200 border-gray-200">
<li className="px-4 py-2"> <li className="px-4 py-2">
<Link href={`${DEVELOPER_DOCS}/self-hosting/installation`}> <Link
<a className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> href={`${DEVELOPER_DOCS}/self-hosting/installation`}
<div className="flex-shrink-0"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50"> <div className="flex-shrink-0">
<Icon.FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" /> <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" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-base font-medium text-gray-900">
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
{t("prisma_studio_tip")}
</span> </span>
</div> </h3>
<div className="min-w-0 flex-1"> <p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p>
<h3 className="text-base font-medium text-gray-900"> </div>
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2"> <div className="flex-shrink-0 self-center">
<span className="absolute inset-0" aria-hidden="true" /> <Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
{t("prisma_studio_tip")} </div>
</span>
</h3>
<p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p>
</div>
<div className="flex-shrink-0 self-center">
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
</a>
</Link> </Link>
</li> </li>
<li className="px-4 py-2"> <li className="px-4 py-2">
@@ -167,11 +167,11 @@ export default function Custom404() {
</li> </li>
</ul> </ul>
<div className="mt-8"> <div className="mt-8">
<Link href={`${WEBSITE_URL}/enterprise`}> <Link
<a className="text-base font-medium text-black hover:text-gray-500"> href={`${WEBSITE_URL}/enterprise`}
{t("contact_sales")} className="text-base font-medium text-black hover:text-gray-500">
<span aria-hidden="true"> &rarr;</span> {t("contact_sales")}
</a> <span aria-hidden="true"> &rarr;</span>
</Link> </Link>
</div> </div>
</div> </div>
@@ -243,26 +243,26 @@ export default function Custom404() {
<ul role="list" className="mt-4 divide-y divide-gray-200 border-gray-200"> <ul role="list" className="mt-4 divide-y divide-gray-200 border-gray-200">
{links.map((link, linkIdx) => ( {links.map((link, linkIdx) => (
<li key={linkIdx} className="px-4 py-2"> <li key={linkIdx} className="px-4 py-2">
<Link href={link.href}> <Link
<a className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> href={link.href}
<div className="flex-shrink-0"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50"> <div className="flex-shrink-0">
<link.icon className="h-6 w-6 text-gray-700" aria-hidden="true" /> <span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
<link.icon className="h-6 w-6 text-gray-700" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-base font-medium text-gray-900">
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
{link.title}
</span> </span>
</div> </h3>
<div className="min-w-0 flex-1"> <p className="text-base text-gray-500">{link.description}</p>
<h3 className="text-base font-medium text-gray-900"> </div>
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2"> <div className="flex-shrink-0 self-center">
<span className="absolute inset-0" aria-hidden="true" /> <Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
{link.title} </div>
</span>
</h3>
<p className="text-base text-gray-500">{link.description}</p>
</div>
<div className="flex-shrink-0 self-center">
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
</a>
</Link> </Link>
</li> </li>
))} ))}
@@ -313,11 +313,9 @@ export default function Custom404() {
</li> </li>
</ul> </ul>
<div className="mt-8"> <div className="mt-8">
<Link href="/"> <Link href="/" className="text-base font-medium text-black hover:text-gray-500">
<a className="text-base font-medium text-black hover:text-gray-500"> {t("or_go_back_home")}
{t("or_go_back_home")} <span aria-hidden="true"> &rarr;</span>
<span aria-hidden="true"> &rarr;</span>
</a>
</Link> </Link>
</div> </div>
</div> </div>
+39 -44
View File
@@ -1,6 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import dynamic from "next/dynamic";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect } from "react";
@@ -27,12 +26,11 @@ 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 } from "@calcom/ui"; import { Icon, HeadSeo, AvatarGroup } from "@calcom/ui";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr"; import { EmbedProps } from "@lib/withEmbedSsr";
import AvatarGroup from "@components/ui/AvatarGroup";
import { AvatarSSR } from "@components/ui/AvatarSSR"; import { AvatarSSR } from "@components/ui/AvatarSSR";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
@@ -58,27 +56,27 @@ 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-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-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" /> <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" />
<Link href={getUsernameSlugLink({ users: props.users, slug: type.slug })}> <Link
<a className="flex justify-between px-6 py-4" data-testid="event-type-link"> href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
<div className="flex-shrink"> className="flex justify-between px-6 py-4"
<p className="dark:text-darkgray-700 text-sm font-semibold text-neutral-900">{type.title}</p> data-testid="event-type-link">
<EventTypeDescription className="text-sm" eventType={type} /> <div className="flex-shrink">
</div> <p className="dark:text-darkgray-700 text-sm font-semibold text-gray-900">{type.title}</p>
<div className="mt-1 self-center"> <EventTypeDescription className="text-sm" eventType={type} />
<AvatarGroup </div>
border="border-2 border-white dark:border-darkgray-100" <div className="mt-1 self-center">
truncateAfter={4} <AvatarGroup
className="flex flex-shrink-0" truncateAfter={4}
size={10} className="flex flex-shrink-0"
items={props.users.map((user) => ({ size="sm"
alt: user.name || "", items={props.users.map((user) => ({
image: user.avatar || "", alt: user.name || "",
}))} image: user.avatar || "",
/> }))}
</div> />
</a> </div>
</Link> </Link>
</li> </li>
))} ))}
@@ -135,20 +133,19 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
{isSingleUser && ( // When we deal with a single user, not dynamic group {isSingleUser && ( // When we deal with a single user, not dynamic group
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<AvatarSSR user={user} className="mx-auto mb-4 h-24 w-24" alt={nameOrUsername} /> <AvatarSSR user={user} className="mx-auto mb-4 h-24 w-24" alt={nameOrUsername} />
<h1 className="font-cal mb-1 text-3xl text-neutral-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" /> <Icon.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-neutral-500">{user.bio}</p> <p className="dark:text-darkgray-600 text-s text-gray-500">{user.bio}</p>
</div> </div>
)} )}
<div <div
className={classNames( className={classNames(
"rounded-md ", "rounded-md ",
!isEventListEmpty && !isEventListEmpty && "border border-gray-200 dark:border-gray-700 dark:hover:border-gray-600"
"border border-neutral-200 dark:border-neutral-700 dark:hover:border-neutral-600"
)} )}
data-testid="event-types"> data-testid="event-types">
{user.away ? ( {user.away ? (
@@ -167,7 +164,7 @@ 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-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-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" /> <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" />
{/* 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
@@ -175,22 +172,20 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
href={{ href={{
pathname: `/${user.username}/${type.slug}`, pathname: `/${user.username}/${type.slug}`,
query, query,
}}> }}
<a onClick={async () => {
onClick={async () => { sdkActionManager?.fire("eventTypeSelected", {
sdkActionManager?.fire("eventTypeSelected", { eventType: type,
eventType: type, });
}); }}
}} className="block w-full p-5"
className="block w-full p-5" data-testid="event-type-link">
data-testid="event-type-link"> <div className="flex flex-wrap items-center">
<div className="flex flex-wrap items-center"> <h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700"> {type.title}
{type.title} </h2>
</h2> </div>
</div> <EventTypeDescription eventType={type} />
<EventTypeDescription eventType={type} />
</a>
</Link> </Link>
</div> </div>
)) ))
+1 -1
View File
@@ -24,7 +24,7 @@ export default function Type(props: AvailabilityPageProps) {
const { t } = useLocale(); const { t } = useLocale();
return props.away ? ( return props.away ? (
<div className="h-screen dark:bg-neutral-900"> <div className="h-screen dark:bg-gray-900">
<main className="mx-auto max-w-3xl px-4 py-24"> <main className="mx-auto max-w-3xl px-4 py-24">
<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">
+2 -2
View File
@@ -27,7 +27,7 @@ export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: BookPageProps) { export default function Book(props: BookPageProps) {
const { t } = useLocale(); const { t } = useLocale();
return props.away ? ( return props.away ? (
<div className="h-screen dark:bg-neutral-900"> <div className="h-screen dark:bg-gray-900">
<main className="mx-auto max-w-3xl px-4 py-24"> <main className="mx-auto max-w-3xl px-4 py-24">
<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">
@@ -42,7 +42,7 @@ export default function Book(props: BookPageProps) {
</main> </main>
</div> </div>
) : props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? ( ) : props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? (
<div className="h-screen dark:bg-neutral-900"> <div className="h-screen dark:bg-gray-900">
<main className="mx-auto max-w-3xl px-4 py-24"> <main className="mx-auto max-w-3xl px-4 py-24">
<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">
+1 -2
View File
@@ -53,7 +53,6 @@ class MyDocument extends Document<Props> {
return ( return (
<Html lang={locale} dir={dir}> <Html lang={locale} dir={dir}>
<Head> <Head>
<link rel="preload" href="/cal.ttf" as="font" type="font/ttf" crossOrigin="anonymous" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
@@ -82,7 +81,7 @@ class MyDocument extends Document<Props> {
</Head> </Head>
<body <body
className="dark:bg-darkgray-50 desktop-transparent bg-gray-100" className="dark:bg-darkgray-50 desktop-transparent bg-gray-100 antialiased"
style={ style={
this.props.isEmbed this.props.isEmbed
? { ? {
+6 -1
View File
@@ -1,10 +1,10 @@
import { IdentityProvider } from "@prisma/client"; import { IdentityProvider } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/lib/auth";
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 { hashPassword } from "@lib/auth";
import slugify from "@lib/slugify"; import slugify from "@lib/slugify";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -12,6 +12,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return; return;
} }
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
res.status(403).json({ message: "Signup is disabled" });
return;
}
const data = req.body; const data = req.body;
const { email, password } = data; const { email, password } = data;
const username = slugify(data.username); const username = slugify(data.username);
@@ -37,6 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
location: true, location: true,
user: { user: {
select: { select: {
id: true,
email: true, email: true,
name: true, name: true,
username: true, username: true,
@@ -100,6 +101,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
startTime: booking.startTime.toISOString(), startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(), endTime: booking.endTime.toISOString(),
organizer: { organizer: {
id: user.id,
email: user.email, email: user.email,
name, name,
timeZone: user.timeZone, timeZone: user.timeZone,
+20 -2
View File
@@ -35,6 +35,24 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
uid: "xxyPr4cg2xx4XoS2KeMEQy", uid: "xxyPr4cg2xx4XoS2KeMEQy",
metadata: {}, metadata: {},
recurringEvent: null, recurringEvent: null,
appsStatus: [
{
appName: "Outlook Calendar",
type: "office365_calendar",
success: 1,
failures: 0,
errors: [],
warnings: [],
},
{
appName: "Google Meet",
type: "conferencing",
success: 0,
failures: 1,
errors: [],
warnings: ["In order to use Google Meet you must set your destination calendar to a Google Calendar"],
},
],
}; };
req.statusCode = 200; req.statusCode = 200;
@@ -42,9 +60,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Content-Type", "text/html"); res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate"); res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write( res.write(
renderEmail("OrganizerRequestEmail", { renderEmail("OrganizerScheduledEmail", {
attendee: evt.attendees[0],
calEvent: evt, calEvent: evt,
attendee: evt.organizer,
}) })
); );
res.end(); res.end();
+1 -1
View File
@@ -2,11 +2,11 @@ import { NextApiRequest, NextApiResponse } from "next";
import type { Session } from "next-auth"; import type { Session } from "next-auth";
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath"; import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
import { getSession } from "@calcom/lib/auth";
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType"; import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler"; import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
const defaultIntegrationAddHandler = async ({ const defaultIntegrationAddHandler = async ({
+27 -28
View File
@@ -1,34 +1,18 @@
import fs from "fs"; import fs from "fs";
import matter from "gray-matter"; import matter from "gray-matter";
import MarkdownIt from "markdown-it";
import { GetStaticPaths, GetStaticPropsContext } from "next"; import { GetStaticPaths, GetStaticPropsContext } from "next";
import { MDXRemote } from "next-mdx-remote";
import { serialize } from "next-mdx-remote/serialize";
import Image from "next/image";
import Link from "next/link";
import path from "path"; import path from "path";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry"; import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import ExisitingGoogleCal from "@calcom/app-store/googlevideo/components/ExistingGoogleCal";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import App from "@components/apps/App"; import App from "@components/apps/App";
const components = { const md = new MarkdownIt("default", { html: true, breaks: true });
a: ({ href = "", ...otherProps }: JSX.IntrinsicElements["a"]) => (
<Link href={href}>
<a {...otherProps} />
</Link>
),
img: ({ src = "", alt = "", placeholder, ...rest }: JSX.IntrinsicElements["img"]) => (
<Image src={src} alt={alt} {...rest} />
),
// @TODO: In v2 the slider isn't shown anymore. However, to ensure the v1 pages keep
// working, this component is still rendered in the MDX content. To skip them in the v2
// content we have to render null here. In v2 the gallery is shown by directly
// using the `items` property from the MDX's meta data.
Slider: () => <></>,
};
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) { function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
return ( return (
@@ -50,10 +34,16 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
email={data.email} email={data.email}
licenseRequired={data.licenseRequired} licenseRequired={data.licenseRequired}
isProOnly={data.isProOnly} isProOnly={data.isProOnly}
images={source?.scope?.items as string[] | undefined} images={source.data?.items as string[] | undefined}
isTemplate={data.isTemplate}
// tos="https://zoom.us/terms" // tos="https://zoom.us/terms"
// privacy="https://zoom.us/privacy" // privacy="https://zoom.us/privacy"
body={<MDXRemote {...source} components={components} />} body={
<>
{data.slug === "google-meet" && <ExisitingGoogleCal />}
<div dangerouslySetInnerHTML={{ __html: md.render(source.content) }} />
</>
}
/> />
); );
} }
@@ -81,25 +71,34 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
if (!singleApp) return { notFound: true }; if (!singleApp) return { notFound: true };
const appDirname = app.dirName; const isTemplate = singleApp.isTemplate;
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/README.mdx`); const appDirname = path.join(isTemplate ? "templates" : "", app.dirName);
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
const postFilePath = path.join(README_PATH); const postFilePath = path.join(README_PATH);
let source = ""; let source = "";
try { try {
/* If the app doesn't have a README we fallback to the package description */
source = fs.readFileSync(postFilePath).toString(); source = fs.readFileSync(postFilePath).toString();
source = source.replace(/{DESCRIPTION}/g, singleApp.description);
} catch (error) { } catch (error) {
console.log(`No README.mdx provided for: ${appDirname}`); /* If the app doesn't have a README we fallback to the package description */
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
source = singleApp.description; source = singleApp.description;
} }
const { content, data } = matter(source); const { content, data } = matter(source);
const mdxSource = await serialize(content, { scope: data }); if (data.items) {
data.items = data.items.map((item: string) => {
if (!item.includes("/api/app-store")) {
// Make relative paths absolute
return `/api/app-store/${appDirname}/${item}`;
}
return item;
});
}
return { return {
props: { props: {
source: mdxSource, source: { content, data },
data: singleApp, data: singleApp,
}, },
}; };
@@ -4,9 +4,10 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { getAppRegistry } from "@calcom/app-store/_appRegistry"; import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { AppCard, Shell, SkeletonText } from "@calcom/ui"; import { AppCard, SkeletonText } from "@calcom/ui";
export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) { export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) {
const { t, isLocaleReady } = useLocale(); const { t, isLocaleReady } = useLocale();
@@ -20,10 +21,10 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
backPath="/apps" backPath="/apps"
heading={ heading={
<> <>
<Link href="/apps"> <Link
<a className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900"> href="/apps"
{isLocaleReady ? t("app_store") : <SkeletonText className="h-4 w-24" />}{" "} className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
</a> {isLocaleReady ? t("app_store") : <SkeletonText className="h-4 w-24" />}{" "}
</Link> </Link>
{category && ( {category && (
<span className="gap-1 text-gray-600"> <span className="gap-1 text-gray-600">
+19 -18
View File
@@ -2,8 +2,9 @@ import { InferGetStaticPropsType } from "next";
import Link from "next/link"; 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 { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon, Shell, SkeletonText } from "@calcom/ui"; import { Icon, SkeletonText } from "@calcom/ui";
export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) { export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) {
const { t, isLocaleReady } = useLocale(); const { t, isLocaleReady } = useLocale();
@@ -11,28 +12,28 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
return ( return (
<Shell isPublic large> <Shell isPublic large>
<div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0"> <div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0">
<Link href="/apps"> <Link
<a className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900"> href="/apps"
<Icon.FiArrowLeft className="h-4 w-4" /> className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "} <Icon.FiArrowLeft className="h-4 w-4" />
</a> {isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
</Link> </Link>
</div> </div>
<div className="mb-16"> <div className="mb-16">
<div className="grid h-auto w-full grid-cols-5 gap-3"> <div className="grid h-auto w-full grid-cols-5 gap-3">
{categories.map((category) => ( {categories.map((category) => (
<Link key={category.name} href={"/apps/categories/" + category.name}> <Link
<a key={category.name}
data-testid={`app-store-category-${category.name}`} href={"/apps/categories/" + category.name}
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block"> data-testid={`app-store-category-${category.name}`}
<div className="self-center"> className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
<h3 className="font-medium capitalize">{category.name}</h3> <div className="self-center">
<p className="text-sm text-gray-500"> <h3 className="font-medium capitalize">{category.name}</h3>
{t("number_apps", { count: category.count })}{" "} <p className="text-sm text-gray-500">
<Icon.FiArrowRight className="inline-block h-4 w-4" /> {t("number_apps", { count: category.count })}{" "}
</p> <Icon.FiArrowRight className="inline-block h-4 w-4" />
</div> </p>
</a> </div>
</Link> </Link>
))} ))}
</div> </div>
+42 -13
View File
@@ -7,12 +7,31 @@ import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/client"; import type { AppCategories } from "@calcom/prisma/client";
import { inferSSRProps } from "@calcom/types/inferSSRProps"; import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { AllApps, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui"; import {
AllApps,
AppStoreCategories,
HorizontalTabItemProps,
HorizontalTabs,
Icon,
TextField,
PopularAppsSlider,
} from "@calcom/ui";
import AppsLayout from "@components/apps/layouts/AppsLayout"; import AppsLayout from "@components/apps/layouts/AppsLayout";
import { ssgInit } from "@server/lib/ssg"; import { ssgInit } from "@server/lib/ssg";
const tabs: HorizontalTabItemProps[] = [
{
name: "app_store",
href: "/apps",
},
{
name: "installed_apps",
href: "/apps/installed",
},
];
function AppsSearch({ function AppsSearch({
onChange, onChange,
className, className,
@@ -43,20 +62,30 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
heading={t("app_store")} heading={t("app_store")}
subtitle={t("app_store_description")} subtitle={t("app_store_description")}
actions={(className) => ( actions={(className) => (
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} /> <div className="flex w-full flex-col md:flex-row md:justify-between lg:w-auto">
<div className="lg:hidden">
<HorizontalTabs tabs={tabs} />
</div>
<div>
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
</div>
</div>
)} )}
headerClassName="sm:hidden lg:block hidden"
emptyStore={!appStore.length}> emptyStore={!appStore.length}>
{!searchText && ( <div className="flex flex-col gap-y-8">
<> {!searchText && (
<AppStoreCategories categories={categories} /> <>
<TrendingAppsSlider items={appStore} /> <AppStoreCategories categories={categories} />
</> <PopularAppsSlider items={appStore} />
)} </>
<AllApps )}
apps={appStore} <AllApps
searchText={searchText} apps={appStore}
categories={categories.map((category) => category.name)} searchText={searchText}
/> categories={categories.map((category) => category.name)}
/>
</div>
</AppsLayout> </AppsLayout>
); );
} }
+23 -21
View File
@@ -15,8 +15,8 @@ import {
EmptyScreen, EmptyScreen,
Icon, Icon,
List, List,
ShellSubHeading,
AppSkeletonLoader as SkeletonLoader, AppSkeletonLoader as SkeletonLoader,
ShellSubHeading,
} from "@calcom/ui"; } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
@@ -92,8 +92,8 @@ function ConnectOrDisconnectIntegrationButton(props: {
} }
interface IntegrationsContainerProps { interface IntegrationsContainerProps {
variant?: keyof typeof InstalledAppVariants; variant?: typeof InstalledAppVariants[number];
exclude?: (keyof typeof InstalledAppVariants)[]; exclude?: typeof InstalledAppVariants[number][];
} }
interface IntegrationsListProps { interface IntegrationsListProps {
@@ -115,6 +115,7 @@ const IntegrationsList = ({ data }: IntegrationsListProps) => {
logo={item.logo} logo={item.logo}
description={item.description} description={item.description}
separate={true} separate={true}
isTemplate={item.isTemplate}
invalidCredential={item.invalidCredentialIds.length > 0} invalidCredential={item.invalidCredentialIds.length > 0}
actions={ actions={
<div className="flex w-16 justify-end"> <div className="flex w-16 justify-end">
@@ -143,8 +144,10 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
automation: Icon.FiShare2, automation: Icon.FiShare2,
analytics: Icon.FiBarChart, analytics: Icon.FiBarChart,
payment: Icon.FiCreditCard, payment: Icon.FiCreditCard,
web3: Icon.FiBarChart,
other: Icon.FiGrid, other: Icon.FiGrid,
}; };
return ( return (
<QueryCell <QueryCell
query={query} query={query}
@@ -183,6 +186,7 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
buttonRaw={ buttonRaw={
<Button <Button
color="secondary" color="secondary"
data-testid={`connect-${variant || "other"}-apps`}
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}> href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
{t(`connect_${variant || "other"}_apps`)} {t(`connect_${variant || "other"}_apps`)}
</Button> </Button>
@@ -197,31 +201,29 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
}; };
const querySchema = z.object({ const querySchema = z.object({
category: z.nativeEnum(InstalledAppVariants), category: z.enum(InstalledAppVariants),
}); });
type querySchemaType = z.infer<typeof querySchema>;
export default function InstalledApps() { export default function InstalledApps() {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
const category = router.query.category; const category = router.query.category as querySchemaType["category"];
const categoryList: querySchemaType["category"][] = [
"payment",
"conferencing",
"automation",
"analytics",
"web3",
];
return ( return (
<InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}> <InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
{(category === InstalledAppVariants.payment || category === InstalledAppVariants.conferencing) && ( {categoryList.includes(category) && <IntegrationsContainer variant={category} />}
<IntegrationsContainer variant={category} /> {category === "calendar" && <CalendarListContainer />}
)} {category === "other" && (
{(category === InstalledAppVariants.automation || category === InstalledAppVariants.analytics) && ( <IntegrationsContainer variant={category} exclude={[...categoryList, "calendar"]} />
<IntegrationsContainer variant={category} />
)}
{category === InstalledAppVariants.calendar && <CalendarListContainer />}
{category === InstalledAppVariants.other && (
<IntegrationsContainer
exclude={[
InstalledAppVariants.conferencing,
InstalledAppVariants.calendar,
InstalledAppVariants.analytics,
InstalledAppVariants.automation,
]}
/>
)} )}
</InstalledAppsLayout> </InstalledAppsLayout>
); );
+1 -1
View File
@@ -40,7 +40,7 @@ export default function Error() {
</div> </div>
</div> </div>
<div className="mt-5 sm:mt-6"> <div className="mt-5 sm:mt-6">
<Link href="/auth/login" passHref> <Link href="/auth/login" passHref legacyBehavior>
<Button className="flex w-full justify-center">{t("go_back_login")}</Button> <Button className="flex w-full justify-center">{t("go_back_login")}</Button>
</Link> </Link>
</div> </div>
+1 -1
View File
@@ -81,7 +81,7 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
<h2 className="text-center text-3xl font-extrabold text-gray-900">{t("request_is_expired")}</h2> <h2 className="text-center text-3xl font-extrabold text-gray-900">{t("request_is_expired")}</h2>
</div> </div>
<p>{t("request_is_expired_instructions")}</p> <p>{t("request_is_expired_instructions")}</p>
<Link href="/auth/forgot-password" passHref> <Link href="/auth/forgot-password" passHref legacyBehavior>
<button <button
type="button" type="button"
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2"> className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
@@ -94,8 +94,8 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
footerText={ footerText={
!success && ( !success && (
<> <>
<Link href="/auth/login"> <Link href="/auth/login" className="font-medium text-gray-900">
<a className="font-medium text-neutral-900">{t("back_to_signin")}</a> {t("back_to_signin")}
</Link> </Link>
</> </>
) )
+12 -5
View File
@@ -115,7 +115,13 @@ export default function Login({
description={t("login")} description={t("login")}
showLogo showLogo
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")} heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
footerText={twoFactorRequired ? TwoFactorFooter : LoginFooter}> footerText={
twoFactorRequired
? TwoFactorFooter
: process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true"
? LoginFooter
: null
}>
<FormProvider {...methods}> <FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} data-testid="login-form"> <form onSubmit={methods.handleSubmit(onSubmit)} data-testid="login-form">
<div> <div>
@@ -133,10 +139,11 @@ export default function Login({
/> />
<div className="relative"> <div className="relative">
<div className="absolute -top-[6px] z-10 ltr:right-0 rtl:left-0"> <div className="absolute -top-[6px] z-10 ltr:right-0 rtl:left-0">
<Link href="/auth/forgot-password"> <Link
<a tabIndex={-1} className="text-sm font-medium text-gray-600"> href="/auth/forgot-password"
{t("forgot")} tabIndex={-1}
</a> className="text-sm font-medium text-gray-600">
{t("forgot")}
</Link> </Link>
</div> </div>
<PasswordField <PasswordField
+1 -1
View File
@@ -27,7 +27,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
{ {
title: t("enable_apps"), title: t("enable_apps"),
description: t("enable_apps_description"), description: t("enable_apps_description"),
content: <AdminAppsList baseURL="/auth/setup?step=2" />, content: <AdminAppsList baseURL="/auth/setup?step=2" useQueryParam={true} />,
isLoading: false, isLoading: false,
}, },
]; ];
+3 -6
View File
@@ -4,6 +4,7 @@ import { z } from "zod";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules"; import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
import Schedule from "@calcom/features/schedules/components/Schedule"; import Schedule from "@calcom/features/schedules/components/Schedule";
import Shell from "@calcom/features/shell/Shell";
import { availabilityAsString } from "@calcom/lib/availability"; import { availabilityAsString } from "@calcom/lib/availability";
import { yyyymmdd } from "@calcom/lib/date-fns"; import { yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -16,7 +17,6 @@ import {
Form, Form,
Icon, Icon,
Label, Label,
Shell,
showToast, showToast,
Skeleton, Skeleton,
SkeletonText, SkeletonText,
@@ -60,7 +60,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
</span> </span>
</Tooltip> </Tooltip>
</h3> </h3>
<p className="mb-4 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p> <p className="mb-4 text-sm text-gray-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p>
<div className="mt-1 space-y-2"> <div className="mt-1 space-y-2">
<DateOverrideList <DateOverrideList
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))} excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
@@ -186,11 +186,8 @@ export default function Availability({ schedule }: { schedule: number }) {
}); });
}} }}
className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6"> className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1 divide-y divide-neutral-200 rounded-md border"> <div className="flex-1 divide-y divide-gray-200 rounded-md border">
<div className=" py-5 sm:p-6"> <div className=" py-5 sm:p-6">
<h3 className="mb-2 px-5 text-base font-medium leading-6 text-gray-900 sm:pl-0">
{t("change_start_end")}
</h3>
{typeof me.data?.weekStart === "string" && ( {typeof me.data?.weekStart === "string" && (
<Schedule <Schedule
control={control} control={control}
+3 -2
View File
@@ -2,9 +2,10 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules"; import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
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, Shell, showToast } from "@calcom/ui"; import { EmptyScreen, Icon, showToast } from "@calcom/ui";
import { withQuery } from "@lib/QueryCell"; import { withQuery } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
@@ -83,7 +84,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
</div> </div>
) : ( ) : (
<div className="mb-16 overflow-hidden rounded-md border border-gray-200 bg-white"> <div className="mb-16 overflow-hidden rounded-md border border-gray-200 bg-white">
<ul className="divide-y divide-neutral-200" data-testid="schedules" ref={animationParentRef}> <ul className="divide-y divide-gray-200" data-testid="schedules" ref={animationParentRef}>
{schedules.map((schedule) => ( {schedules.map((schedule) => (
<ScheduleListItem <ScheduleListItem
displayOptions={{ displayOptions={{
+7 -8
View File
@@ -1,7 +1,8 @@
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
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 { Shell, SkeletonText } from "@calcom/ui"; import { SkeletonText } from "@calcom/ui";
import useRouterQuery from "@lib/hooks/useRouterQuery"; import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -55,7 +56,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
if (e.target.value) setSelectedDate(e.target.value); if (e.target.value) setSelectedDate(e.target.value);
}} }}
/> />
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small> <small className="block text-gray-400">{t("hover_over_bold_times_tip")}</small>
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md"> <div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6"> <div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
@@ -77,17 +78,15 @@ const AvailabilityView = ({ user }: { user: User }) => {
.map((slot: IBusySlot) => ( .map((slot: IBusySlot) => (
<div <div
key={dayjs(slot.start).format("HH:mm")} key={dayjs(slot.start).format("HH:mm")}
className="overflow-hidden rounded-md bg-neutral-100" className="overflow-hidden rounded-md bg-gray-100"
data-testid="troubleshooter-busy-time"> data-testid="troubleshooter-busy-time">
<div className="px-4 py-5 text-black sm:p-6"> <div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "} {t("calendar_shows_busy_between")}{" "}
<span <span className="font-medium text-gray-800" title={dayjs(slot.start).format("HH:mm")}>
className="font-medium text-neutral-800"
title={dayjs(slot.start).format("HH:mm")}>
{dayjs(slot.start).format("HH:mm")} {dayjs(slot.start).format("HH:mm")}
</span>{" "} </span>{" "}
{t("and")}{" "} {t("and")}{" "}
<span className="font-medium text-neutral-800" title={dayjs(slot.end).format("HH:mm")}> <span className="font-medium text-gray-800" title={dayjs(slot.end).format("HH:mm")}>
{dayjs(slot.end).format("HH:mm")} {dayjs(slot.end).format("HH:mm")}
</span>{" "} </span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "} {t("on")} {dayjs(slot.start).format("D")}{" "}
@@ -98,7 +97,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
</div> </div>
)); ));
return ( return (
<div className="overflow-hidden rounded-md bg-neutral-100"> <div className="overflow-hidden rounded-md bg-gray-100">
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div> <div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
</div> </div>
); );
+94 -94
View File
@@ -32,7 +32,7 @@ import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat"; import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage"; import { localStorage } from "@calcom/lib/webstorage";
import prisma, { baseUserSelect } 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, Icon, HeadSeo } from "@calcom/ui";
@@ -322,7 +322,7 @@ export default function Success(props: SuccessProps) {
} }
return t("emailed_you_and_attendees" + titleSuffix); return t("emailed_you_and_attendees" + titleSuffix);
} }
const userIsOwner = !!(session?.user?.id && eventType.users.find((user) => (user.id = session.user.id))); const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
useTheme(isSuccessBookingPage ? props.profile.theme : "light"); useTheme(isSuccessBookingPage ? props.profile.theme : "light");
const title = t( const title = t(
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}` `booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
@@ -353,10 +353,10 @@ export default function Success(props: SuccessProps) {
)} )}
{userIsOwner && !isEmbed && ( {userIsOwner && !isEmbed && (
<div className="mt-2 ml-4 -mb-4"> <div className="mt-2 ml-4 -mb-4">
<Link href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}> <Link
<a 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"> href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}
<Icon.FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")} 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">
</a> <Icon.FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
</Link> </Link>
</div> </div>
)} )}
@@ -379,7 +379,7 @@ export default function Success(props: SuccessProps) {
<div <div
className={classNames( className={classNames(
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl", "main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border-neutral-700", isBackgroundTransparent ? "" : "dark:bg-darkgray-100 dark:border-darkgray-200 bg-white",
"px-8 pt-5 pb-4 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle" "px-8 pt-5 pb-4 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
)} )}
role="dialog" role="dialog"
@@ -408,7 +408,7 @@ export default function Success(props: SuccessProps) {
</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
className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white" className="text-2xl font-semibold leading-6 text-gray-900 dark:text-white"
data-testid={isCancelled ? "cancelled-headline" : ""} data-testid={isCancelled ? "cancelled-headline" : ""}
id="modal-headline"> id="modal-headline">
{needsConfirmation && !isCancelled {needsConfirmation && !isCancelled
@@ -422,9 +422,10 @@ export default function Success(props: SuccessProps) {
: t("meeting_is_scheduled")} : t("meeting_is_scheduled")}
</h3> </h3>
<div className="mt-3"> <div className="mt-3">
<p className="text-neutral-600 dark:text-gray-300">{getTitle()}</p> <p className="text-gray-600 dark:text-gray-300">{getTitle()}</p>
</div> </div>
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-300 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-200 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
{(isCancelled || reschedule) && cancellationReason && ( {(isCancelled || reschedule) && cancellationReason && (
<> <>
<div className="font-medium"> <div className="font-medium">
@@ -499,7 +500,7 @@ export default function Success(props: SuccessProps) {
<> <>
<div className="mt-9 font-medium">{t("additional_notes")}</div> <div className="mt-9 font-medium">{t("additional_notes")}</div>
<div className="col-span-2 mb-2 mt-9"> <div className="col-span-2 mb-2 mt-9">
<p>{bookingInfo.description}</p> <p className="break-words">{bookingInfo.description}</p>
</div> </div>
</> </>
)} )}
@@ -573,7 +574,9 @@ export default function Success(props: SuccessProps) {
{!props.recurringBookings && ( {!props.recurringBookings && (
<span className="text-bookinglight inline text-gray-700"> <span className="text-bookinglight inline text-gray-700">
<span className="underline"> <span className="underline">
<Link href={`/reschedule/${bookingInfo?.uid}`}>{t("reschedule")}</Link> <Link href={`/reschedule/${bookingInfo?.uid}`} legacyBehavior>
{t("reschedule")}
</Link>
</span> </span>
<span className="mx-2">{t("or_lowercase")}</span> <span className="mx-2">{t("or_lowercase")}</span>
</span> </span>
@@ -592,7 +595,7 @@ export default function Success(props: SuccessProps) {
</> </>
) : ( ) : (
<> <>
<hr className="border-bookinglightest dark:border-darkgray-300" /> <hr className="border-bookinglightest dark:border-darkgray-200" />
<CancelBooking <CancelBooking
booking={{ uid: bookingInfo?.uid, title: bookingInfo?.title, id: bookingInfo?.id }} booking={{ uid: bookingInfo?.uid, title: bookingInfo?.title, id: bookingInfo?.id }}
profile={{ name: props.profile.name, slug: props.profile.slug }} profile={{ name: props.profile.name, slug: props.profile.slug }}
@@ -633,17 +636,16 @@ export default function Success(props: SuccessProps) {
? "&recur=" + ? "&recur=" +
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString()) encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
: "") : "")
}> }
<a className="h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 ltr:mr-2 rtl:ml-2 dark:border-neutral-700 dark:text-white"> className="h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 ltr:mr-2 rtl:ml-2 dark:border-gray-700 dark:text-white">
<svg <svg
className="-mt-1.5 inline-block h-4 w-4" className="-mt-1.5 inline-block h-4 w-4"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"> viewBox="0 0 24 24">
<title>Google</title> <title>Google</title>
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" /> <path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
</svg> </svg>
</a>
</Link> </Link>
<Link <Link
href={ href={
@@ -657,19 +659,17 @@ export default function Success(props: SuccessProps) {
"&subject=" + "&subject=" +
eventName eventName
) + (location ? "&location=" + location : "") ) + (location ? "&location=" + location : "")
}> }
<a className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white" target="_blank">
target="_blank"> <svg
<svg className="mr-1 -mt-1.5 inline-block h-4 w-4"
className="mr-1 -mt-1.5 inline-block h-4 w-4" fill="currentColor"
fill="currentColor" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24"> <title>Microsoft Outlook</title>
<title>Microsoft Outlook</title> <path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" /> </svg>
</svg>
</a>
</Link> </Link>
<Link <Link
href={ href={
@@ -683,34 +683,31 @@ export default function Success(props: SuccessProps) {
"&subject=" + "&subject=" +
eventName eventName
) + (location ? "&location=" + location : "") ) + (location ? "&location=" + location : "")
}> }
<a className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white" target="_blank">
target="_blank"> <svg
<svg className="mr-1 -mt-1.5 inline-block h-4 w-4"
className="mr-1 -mt-1.5 inline-block h-4 w-4" fill="currentColor"
fill="currentColor" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24"> <title>Microsoft Office</title>
<title>Microsoft Office</title> <path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" /> </svg>
</svg>
</a>
</Link> </Link>
<Link href={"data:text/calendar," + eventLink()}> <Link
<a href={"data:text/calendar," + eventLink()}
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white" className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
download={props.eventType.title + ".ics"}> download={props.eventType.title + ".ics"}>
<svg <svg
version="1.1" version="1.1"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1000 1000" viewBox="0 0 1000 1000"
className="mr-1 -mt-1.5 inline-block h-4 w-4"> className="mr-1 -mt-1.5 inline-block h-4 w-4">
<title>{t("other")}</title> <title>{t("other")}</title>
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" /> <path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
</svg> </svg>
</a>
</Link> </Link>
</div> </div>
</div> </div>
@@ -849,6 +846,17 @@ export function RecurringBookings({
} }
const getEventTypesFromDB = async (id: number) => { const getEventTypesFromDB = async (id: number) => {
const userSelect = {
id: true,
name: true,
username: true,
hideBranding: true,
theme: true,
brandColor: true,
darkBrandColor: true,
email: true,
timeZone: true,
};
const eventType = await prisma.eventType.findUnique({ const eventType = await prisma.eventType.findUnique({
where: { where: {
id, id,
@@ -867,17 +875,17 @@ const getEventTypesFromDB = async (id: number) => {
locations: true, locations: true,
price: true, price: true,
currency: true, currency: true,
owner: {
select: userSelect,
},
users: { users: {
select: userSelect,
},
hosts: {
select: { select: {
id: true, user: {
name: true, select: userSelect,
username: true, },
hideBranding: true,
theme: true,
brandColor: true,
darkBrandColor: true,
email: true,
timeZone: true,
}, },
}, },
team: { team: {
@@ -921,6 +929,7 @@ const schema = z.object({
uid: z.string(), uid: z.string(),
email: z.string().optional(), email: z.string().optional(),
eventTypeSlug: z.string().optional(), eventTypeSlug: z.string().optional(),
cancel: stringToBoolean,
}); });
const handleSeatsEventTypeOnBooking = ( const handleSeatsEventTypeOnBooking = (
@@ -954,7 +963,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context); const ssr = await ssrInit(context);
const parsedQuery = schema.safeParse(context.query); const parsedQuery = schema.safeParse(context.query);
if (!parsedQuery.success) return { notFound: true }; if (!parsedQuery.success) return { notFound: true };
const { uid, email, eventTypeSlug } = parsedQuery.data; const { uid, email, eventTypeSlug, cancel } = parsedQuery.data;
const bookingInfo = await prisma.booking.findFirst({ const bookingInfo = await prisma.booking.findFirst({
where: { where: {
@@ -1017,27 +1026,18 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}; };
} }
if (!eventTypeRaw.users.length && eventTypeRaw.userId) { eventTypeRaw.users = !!eventTypeRaw.hosts?.length
// TODO we should add `user User` relation on `EventType` so this extra query isn't needed ? eventTypeRaw.hosts.map((host) => host.user)
const user = await prisma.user.findUnique({ : eventTypeRaw.users;
where: {
id: eventTypeRaw.userId,
},
select: baseUserSelect,
});
if (user) {
eventTypeRaw.users.push({
...user,
avatar: "",
allowDynamicBooking: true,
});
}
}
if (!eventTypeRaw.users.length) { if (!eventTypeRaw.users.length) {
return { if (!eventTypeRaw.owner)
notFound: true, return {
}; notFound: true,
};
eventTypeRaw.users.push({
...eventTypeRaw.owner,
});
} }
const eventType = { const eventType = {
@@ -1049,7 +1049,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs), customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
}; };
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl) { if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl && !cancel) {
return { return {
redirect: { redirect: {
destination: eventType.successRedirectUrl, destination: eventType.successRedirectUrl,
+26 -11
View File
@@ -3,7 +3,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PeriodType, SchedulingType } from "@prisma/client"; import { PeriodType, SchedulingType } from "@prisma/client";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -15,6 +14,7 @@ import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
import findDurationType from "@calcom/lib/findDurationType"; 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 { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -64,7 +64,6 @@ export type FormValues = {
phone?: string; phone?: string;
}[]; }[];
customInputs: CustomInputParsed[]; customInputs: CustomInputParsed[];
users: string[];
schedule: number; schedule: number;
periodType: PeriodType; periodType: PeriodType;
periodDays: number; periodDays: number;
@@ -85,6 +84,8 @@ export type FormValues = {
}; };
successRedirectUrl: string; successRedirectUrl: string;
bookingLimits?: BookingLimit; bookingLimits?: BookingLimit;
hosts: { userId: number }[];
hostsFixed: { userId: number }[];
}; };
export type CustomInputParsed = typeof customInputSchema._output; export type CustomInputParsed = typeof customInputSchema._output;
@@ -111,15 +112,15 @@ export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
const EventTypePage = (props: EventTypeSetupProps) => { const EventTypePage = (props: EventTypeSetupProps) => {
const { t } = useLocale(); const { t } = useLocale();
const utils = trpc.useContext(); const utils = trpc.useContext();
const router = useRouter(); const {
const { tabName } = querySchema.parse(router.query); data: { tabName },
} = useTypedQuery(querySchema);
const { data: eventTypeApps } = trpc.viewer.apps.useQuery({ const { data: eventTypeApps } = trpc.viewer.apps.useQuery({
extendsFeature: "EventType", extendsFeature: "EventType",
}); });
const { eventType, locationOptions, team, teamMembers, currentUserMembership } = props; const { eventType, locationOptions, team, teamMembers, currentUserMembership } = props;
const [animationParentRef] = useAutoAnimate<HTMLDivElement>(); const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
const updateMutation = trpc.viewer.eventTypes.update.useMutation({ const updateMutation = trpc.viewer.eventTypes.update.useMutation({
@@ -185,6 +186,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
description: eventType.description ?? undefined, description: eventType.description ?? undefined,
schedule: eventType.schedule || undefined, schedule: eventType.schedule || undefined,
bookingLimits: eventType.bookingLimits || undefined, bookingLimits: eventType.bookingLimits || undefined,
length: eventType.length,
hidden: eventType.hidden, hidden: eventType.hidden,
periodDates: { periodDates: {
startDate: periodDates.startDate, startDate: periodDates.startDate,
@@ -194,12 +196,17 @@ const EventTypePage = (props: EventTypeSetupProps) => {
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0", periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
schedulingType: eventType.schedulingType, schedulingType: eventType.schedulingType,
minimumBookingNotice: eventType.minimumBookingNotice, minimumBookingNotice: eventType.minimumBookingNotice,
minimumBookingNoticeInDurationType: convertToNewDurationType(
"minutes",
findDurationType(eventType.minimumBookingNotice),
eventType.minimumBookingNotice
),
metadata, metadata,
hosts: !!eventType.hosts?.length
? eventType.hosts.filter((host) => !host.isFixed)
: eventType.users
.filter(() => eventType.schedulingType === SchedulingType.ROUND_ROBIN)
.map((user) => ({ userId: user.id })),
hostsFixed: !!eventType.hosts?.length
? eventType.hosts.filter((host) => host.isFixed)
: eventType.users
.filter(() => eventType.schedulingType === SchedulingType.COLLECTIVE)
.map((user) => ({ userId: user.id })),
}, },
resolver: zodResolver( resolver: zodResolver(
z z
@@ -237,7 +244,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
/> />
), ),
availability: <AvailabilityTab isTeamEvent={!!team} />, availability: <AvailabilityTab isTeamEvent={!!team} />,
team: <EventTeamTab eventType={eventType} teamMembers={teamMembers} team={team} />, team: <EventTeamTab teamMembers={teamMembers} team={team} />,
limits: <EventLimitsTab eventType={eventType} />, limits: <EventLimitsTab eventType={eventType} />,
advanced: <EventAdvancedTab eventType={eventType} team={team} />, advanced: <EventAdvancedTab eventType={eventType} team={team} />,
recurring: <EventRecurringTab eventType={eventType} />, recurring: <EventRecurringTab eventType={eventType} />,
@@ -278,6 +285,8 @@ const EventTypePage = (props: EventTypeSetupProps) => {
locations, locations,
metadata, metadata,
customInputs, customInputs,
hosts: hostsInput,
hostsFixed,
// We don't need to send send these values to the backend // We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
seatsPerTimeSlotEnabled, seatsPerTimeSlotEnabled,
@@ -286,6 +295,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
...input ...input
} = values; } = values;
const hosts: (typeof hostsInput[number] & { isFixed?: boolean })[] = [];
if (hostsInput || hostsFixed) {
hosts.push(...hostsInput.concat(hostsFixed.map((host) => ({ isFixed: true, ...host }))));
}
if (bookingLimits) { if (bookingLimits) {
const isValid = validateBookingLimitOrder(bookingLimits); const isValid = validateBookingLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error")); if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
@@ -303,6 +317,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
updateMutation.mutate({ updateMutation.mutate({
...input, ...input,
hosts,
locations, locations,
recurringEvent, recurringEvent,
periodStartDate: periodDates.startDate, periodStartDate: periodDates.startDate,
+74 -79
View File
@@ -8,6 +8,7 @@ import {
CreateEventTypeButton, CreateEventTypeButton,
EventTypeDescriptionLazy as EventTypeDescription, EventTypeDescriptionLazy as EventTypeDescription,
} from "@calcom/features/eventtypes/components"; } from "@calcom/features/eventtypes/components";
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 { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react"; import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
@@ -26,9 +27,10 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
EmptyScreen, EmptyScreen,
Icon, Icon,
Shell,
showToast, showToast,
Switch, Switch,
Avatar,
AvatarGroup,
Tooltip, Tooltip,
} from "@calcom/ui"; } from "@calcom/ui";
@@ -37,8 +39,6 @@ import { HttpError } from "@lib/core/http/error";
import { EmbedButton, EmbedDialog } from "@components/Embed"; import { EmbedButton, EmbedDialog } from "@components/Embed";
import SkeletonLoader from "@components/eventtype/SkeletonLoader"; import SkeletonLoader from "@components/eventtype/SkeletonLoader";
import Avatar from "@components/ui/Avatar";
import AvatarGroup from "@components/ui/AvatarGroup";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
@@ -64,27 +64,26 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
const { t } = useLocale(); const { t } = useLocale();
return ( return (
<Link href={`/event-types/${type.id}?tabName=setup`}> <Link
<a href={`/event-types/${type.id}?tabName=setup`}
className="flex-1 overflow-hidden pr-4 text-sm" className="flex-1 overflow-hidden pr-4 text-sm"
title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}> title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}>
<div> <div>
<span <span
className="font-semibold text-gray-700 ltr:mr-1 rtl:ml-1" className="font-semibold text-gray-700 ltr:mr-1 rtl:ml-1"
data-testid={"event-type-title-" + type.id}> data-testid={"event-type-title-" + type.id}>
{type.title} {type.title}
</span>
<small
className="hidden font-normal leading-4 text-gray-600 sm:inline"
data-testid={"event-type-slug-" + type.id}>{`/${group.profile.slug}/${type.slug}`}</small>
{readOnly && (
<span className="rtl:ml-2inline items-center rounded-sm bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 ltr:ml-2 ltr:mr-2">
{t("readonly")}
</span> </span>
<small )}
className="hidden font-normal leading-4 text-gray-600 sm:inline" </div>
data-testid={"event-type-slug-" + type.id}>{`/${group.profile.slug}/${type.slug}`}</small> <EventTypeDescription eventType={type} />
{readOnly && (
<span className="rtl:ml-2inline items-center rounded-sm bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 ltr:ml-2 ltr:mr-2">
{t("readonly") as string}
</span>
)}
</div>
<EventTypeDescription eventType={type} />
</a>
</Link> </Link>
); );
}; };
@@ -252,7 +251,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
const lastItem = types[types.length - 1]; const lastItem = types[types.length - 1];
return ( return (
<div className="mb-16 flex overflow-hidden rounded-md border border-gray-200 bg-white"> <div className="mb-16 flex overflow-hidden rounded-md border border-gray-200 bg-white">
<ul ref={parent} className="!static w-full divide-y divide-neutral-200" data-testid="event-types"> <ul ref={parent} className="!static w-full divide-y divide-gray-200" data-testid="event-types">
{types.map((type, index) => { {types.map((type, index) => {
const embedLink = `${group.profile.slug}/${type.slug}`; const embedLink = `${group.profile.slug}/${type.slug}`;
const calLink = `${CAL_URL}/${embedLink}`; const calLink = `${CAL_URL}/${embedLink}`;
@@ -280,9 +279,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<div className="flex justify-between space-x-2 rtl:space-x-reverse"> <div className="flex justify-between space-x-2 rtl:space-x-reverse">
{type.users?.length > 1 && ( {type.users?.length > 1 && (
<AvatarGroup <AvatarGroup
border="border-2 border-white"
className="relative top-1 right-3" className="relative top-1 right-3"
size={8} size="sm"
truncateAfter={4} truncateAfter={4}
items={type.users.map((organizer) => ({ items={type.users.map((organizer) => ({
alt: organizer.name || "", alt: organizer.name || "",
@@ -297,7 +295,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{t("hidden")} {t("hidden")}
</Badge> </Badge>
)} )}
<Tooltip content={t("show_eventtype_on_profile") as string}> <Tooltip content={t("show_eventtype_on_profile")}>
<div className="self-center rounded-md p-2 hover:bg-gray-200"> <div className="self-center rounded-md p-2 hover:bg-gray-200">
<Switch <Switch
name="Hidden" name="Hidden"
@@ -310,20 +308,20 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</Tooltip> </Tooltip>
<ButtonGroup combined> <ButtonGroup combined>
<Tooltip content={t("preview") as string}> <Tooltip content={t("preview")}>
<Button <Button
color="secondary" color="secondary"
target="_blank" target="_blank"
size="icon" variant="icon"
href={calLink} href={calLink}
StartIcon={Icon.FiExternalLink} StartIcon={Icon.FiExternalLink}
/> />
</Tooltip> </Tooltip>
<Tooltip content={t("copy_link") as string}> <Tooltip content={t("copy_link")}>
<Button <Button
color="secondary" color="secondary"
size="icon" variant="icon"
StartIcon={Icon.FiLink} StartIcon={Icon.FiLink}
onClick={() => { onClick={() => {
showToast(t("link_copied"), "success"); showToast(t("link_copied"), "success");
@@ -338,7 +336,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
className="radix-state-open:rounded-r-md"> className="radix-state-open:rounded-r-md">
<Button <Button
type="button" type="button"
size="icon" variant="icon"
color="secondary" color="secondary"
StartIcon={Icon.FiMoreHorizontal} StartIcon={Icon.FiMoreHorizontal}
/> />
@@ -350,7 +348,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
data-testid={"event-type-edit-" + type.id} data-testid={"event-type-edit-" + type.id}
StartIcon={Icon.FiEdit2} StartIcon={Icon.FiEdit2}
onClick={() => router.push("/event-types/" + type.id)}> onClick={() => router.push("/event-types/" + type.id)}>
{t("edit") as string} {t("edit")}
</DropdownItem> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
@@ -359,7 +357,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
data-testid={"event-type-duplicate-" + type.id} data-testid={"event-type-duplicate-" + type.id}
StartIcon={Icon.FiCopy} StartIcon={Icon.FiCopy}
onClick={() => openDuplicateModal(type)}> onClick={() => openDuplicateModal(type)}>
{t("duplicate") as string} {t("duplicate")}
</DropdownItem> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
@@ -372,18 +370,19 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{t("embed")} {t("embed")}
</EmbedButton> </EmbedButton>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" /> <DropdownMenuSeparator />
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */} {/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) && ( {(group.metadata?.readOnly === false || group.metadata.readOnly === null) && (
<DropdownMenuItem> <DropdownMenuItem>
<DropdownItem <DropdownItem
color="destructive"
onClick={() => { onClick={() => {
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id); setDeleteDialogTypeId(type.id);
}} }}
StartIcon={Icon.FiTrash} StartIcon={Icon.FiTrash}
className="w-full rounded-none"> className="w-full rounded-none">
{t("delete") as string} {t("delete")}
</DropdownItem> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
)} )}
@@ -397,20 +396,23 @@ 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 type="button" size="icon" color="secondary" StartIcon={Icon.FiMoreHorizontal} /> <Button
type="button"
variant="icon"
color="secondary"
StartIcon={Icon.FiMoreHorizontal}
/>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuPortal> <DropdownMenuPortal>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
<Link href={calLink}> <Link href={calLink} target="_blank">
<a target="_blank"> <Button
<Button color="minimal"
color="minimal" StartIcon={Icon.FiExternalLink}
StartIcon={Icon.FiExternalLink} className="w-full rounded-none">
className="w-full rounded-none"> {t("preview")}
{t("preview") as string} </Button>
</Button>
</a>
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
@@ -424,7 +426,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
navigator.clipboard.writeText(calLink); navigator.clipboard.writeText(calLink);
showToast(t("link_copied"), "success"); showToast(t("link_copied"), "success");
}}> }}>
{t("copy_link") as string} {t("copy_link")}
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
{isNativeShare ? ( {isNativeShare ? (
@@ -445,7 +447,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
.then(() => showToast(t("link_shared"), "success")) .then(() => showToast(t("link_shared"), "success"))
.catch(() => showToast(t("failed"), "error")); .catch(() => showToast(t("failed"), "error"));
}}> }}>
{t("share") as string} {t("share")}
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
) : null} ) : null}
@@ -456,7 +458,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
color="minimal" color="minimal"
className="w-full rounded-none" className="w-full rounded-none"
StartIcon={Icon.FiEdit}> StartIcon={Icon.FiEdit}>
{t("edit") as string} {t("edit")}
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
@@ -467,10 +469,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
data-testid={"event-type-duplicate-" + type.id} data-testid={"event-type-duplicate-" + type.id}
StartIcon={Icon.FiCopy} StartIcon={Icon.FiCopy}
onClick={() => openDuplicateModal(type)}> onClick={() => openDuplicateModal(type)}>
{t("duplicate") as string} {t("duplicate")}
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" /> <DropdownMenuSeparator />
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
<Button <Button
onClick={() => { onClick={() => {
@@ -480,7 +482,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
color="destructive" color="destructive"
StartIcon={Icon.FiTrash} StartIcon={Icon.FiTrash}
className="w-full rounded-none"> className="w-full rounded-none">
{t("delete") as string} {t("delete")}
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@@ -502,7 +504,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
e.preventDefault(); e.preventDefault();
deleteEventTypeHandler(deleteDialogTypeId); deleteEventTypeHandler(deleteDialogTypeId);
}}> }}>
{t("delete_event_type_description") as string} {t("delete_event_type_description")}
</ConfirmationDialogContent> </ConfirmationDialogContent>
</Dialog> </Dialog>
</div> </div>
@@ -515,38 +517,34 @@ const EventTypeListHeading = ({
teamId, teamId,
}: EventTypeListHeadingProps): JSX.Element => { }: EventTypeListHeadingProps): JSX.Element => {
return ( return (
<div className="mb-4 flex"> <div className="mb-4 flex items-center space-x-2">
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}> <Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
<a> <Avatar
<Avatar alt={profile?.name || ""}
alt={profile?.name || ""} imageSrc={`${WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
imageSrc={`${WEBAPP_URL}/${profile.slug}/avatar.png` || undefined} size="sm"
size={8} className="mt-1 inline ltr:mr-2 rtl:ml-2"
className="mt-1 inline ltr:mr-2 rtl:ml-2" />
/>
</a>
</Link> </Link>
<div> <div>
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}> <Link
<a className="font-bold">{profile?.name || ""}</a> href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
className="font-bold">
{profile?.name || ""}
</Link> </Link>
{membershipCount && teamId && ( {membershipCount && teamId && (
<span className="relative -top-px text-xs text-neutral-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`}>
<a> <Badge variant="gray">
<Badge variant="gray"> <Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" />
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" /> {membershipCount}
{membershipCount} </Badge>
</Badge>
</a>
</Link> </Link>
</span> </span>
)} )}
{profile?.slug && ( {profile?.slug && (
<Link href={`${CAL_URL}/${profile.slug}`}> <Link href={`${CAL_URL}/${profile.slug}`} className="block text-xs text-gray-500">
<a className="block text-xs text-neutral-500">{`${CAL_URL?.replace("https://", "")}/${ {`${CAL_URL?.replace("https://", "")}/${profile.slug}`}
profile.slug
}`}</a>
</Link> </Link>
)} )}
</div> </div>
@@ -581,10 +579,7 @@ const EventTypesPage = () => {
return ( return (
<div> <div>
<Shell <Shell heading={t("event_types_page_title")} subtitle={t("event_types_page_subtitle")} CTA={<CTA />}>
heading={t("event_types_page_title") as string}
subtitle={t("event_types_page_subtitle") as string}
CTA={<CTA />}>
<WithQuery <WithQuery
customLoader={<SkeletonLoader />} customLoader={<SkeletonLoader />}
success={({ data }) => ( success={({ data }) => (
@@ -178,6 +178,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
throw new Error("User from session not found"); throw new Error("User from session not found");
} }
if (user.completedOnboarding) {
return { redirect: { permanent: false, destination: "/event-types" } };
}
return { return {
props: { props: {
...(await serverSideTranslations(context.locale ?? "", ["common"])), ...(await serverSideTranslations(context.locale ?? "", ["common"])),
+1 -1
View File
@@ -1,5 +1,5 @@
import Shell, { MobileNavigationMoreItems } from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MobileNavigationMoreItems, Shell } from "@calcom/ui";
export default function MorePage() { export default function MorePage() {
const { t } = useLocale(); const { t } = useLocale();
Loaded 100 of 636 files, more files were not shown because too many files have changed in this diff. Show more