Compare commits

...

106 Commits

Author SHA1 Message Date
Alex van Andel 0fad273741 Merged with main 2023-08-01 13:46:40 +01:00
Crowdin Bot eeabeb355d New Crowdin translations by Github Action 2023-08-01 11:53:40 +01:00
Ritesh Patil b92303b59f fix: embed hides floating button (#9165)
* fix: embed - hides floating button on click to prevent overlapping over modal

* revert changes on yarn.lock

* z-index for Modal wasnt highest

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-01 11:53:40 +01:00
Leo Giovanetti ce93355c16 fix: Unpublished screens (#10453)
* Implementation

* Changes and e2e

* Reverting launch.json

* Reverting org create handler

* Reverting yarn.lock

* DRYness and nitpicks

* Default org domain to undefined

* Applying zomars suggestion

* Suggestions

* Fixing seed and type in suggestion

* Fixing types

---------

Co-authored-by: zomars <zomars@me.com>
2023-08-01 11:53:40 +01:00
Pratik Kumar 9778a30afb Fix private link glitch (#10456) 2023-08-01 11:53:40 +01:00
Bhargav 373ec296bf fix: Fixed 10080 settings sidebar scrolling issue (#10471)
* fix: #10080 settings sidebar scrolling issue

* fix: type error

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

* Fixed type error

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-08-01 11:53:40 +01:00
Varun Thummar 1e7209b81b fix: added validation for empty reset password field (#10464)
* added validation for empty reset password field

* removed useState and useEffect

---------

Co-authored-by: varun thummar <Varun>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-08-01 11:53:40 +01:00
Rama Krishna Reddy e7220c4bdd feat: Verify email of people setting a meeting with you (#10317)
* booker email verification changes

* name type fix

* use totp and code

* prisma schema styling

* refactor: code

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

* fix: book event form

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

* fix: type error

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

* fix: type errors

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

* fix: unit tests

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

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

* fix: type error

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: rkreddy99 <rreddy@e2clouds.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-01 11:53:40 +01:00
Carina Wollendorfer 682d4793f5 fix: temporarily disable sms/whatsapp to attendee workflow action (#10485)
* disable sms and whatsapp to attendee

* fix type error

* fix prisma enum imports

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-08-01 11:53:17 +01:00
Alex van Andel 6e396bc65f Fixed workflowReminder test + refactored reminderSchedule (#10487) 2023-08-01 11:53:17 +01:00
Rama Krishna Reddy 7850131b0b fix: do not delete last schedule (#10444)
Co-authored-by: rkreddy99 <rreddy@e2clouds.com>
2023-08-01 11:53:17 +01:00
Crowdin Bot 2ed3e09c20 New Crowdin translations by Github Action 2023-08-01 11:53:17 +01:00
Shivam Kalra c5dd667ef9 chore: team booking test (#10440)
* feat: add team name in the createTeam fixture

* chore: remove unnecessary variables

* test: e2e booking for Round Robin EventType

* feat: teammates in user fixtures

* chore: DRY

* fix: remove redundant todo

* fix: add teamEvent title and slug in scenario

* test: booking title, RR hostname
2023-08-01 11:53:17 +01:00
Crowdin Bot 990e6fa52d New Crowdin translations by Github Action 2023-08-01 11:53:17 +01:00
Alex van Andel c5dadb3d7d chore: Remove extra params from the users prop. (#10411)
* Removes extra params from the users prop.

* context no longer requires explicit type

* Remove user.avatar prop
2023-08-01 11:53:17 +01:00
Alex van Andel fd880c80a6 Remove IP ban and tweak matcher accordingly (#10418) 2023-08-01 11:53:08 +01:00
Crowdin Bot fdb629be53 New Crowdin translations by Github Action 2023-08-01 11:53:08 +01:00
sajanlamsal 2eea0d2731 fix: changed Japanese translations on booking page to better match the scenario. (#10416) 2023-08-01 11:53:08 +01:00
Crowdin Bot c5fccf357b New Crowdin translations by Github Action 2023-08-01 11:53:08 +01:00
Shivam Kalra 6688f8d89c test: Round Robin Team Booking (#10413) 2023-08-01 11:52:59 +01:00
Crowdin Bot f89ba37e0e New Crowdin translations by Github Action 2023-08-01 11:52:35 +01:00
Hariom Balhara bce96f7c87 fix: middleware logic Routing Forms (#10412)
* Match /apps/routing_forms

* Next.js config.matcher doesnt support spread operator
2023-08-01 11:52:21 +01:00
Hariom Balhara c2815e52ce fix: Make routing-form a preinstalled app (#9836)
Co-authored-by: zomars <zomars@me.com>
2023-08-01 11:51:57 +01:00
mohammed hussam b5e37c3175 feat: dynamic group links + flexible schedule (#9931)
* redirect user to /[user]/[type] for dynamic booking in /[user]

* feat: dynamic group links

* fix: type errors

* fix: more type errors

* Booker components cannot rely on useRouter

* Rename 15min/30/60 to dynamic

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

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-08-01 11:51:33 +01:00
Crowdin Bot e08fc09fee New Crowdin translations by Github Action 2023-08-01 11:51:23 +01:00
Crowdin Bot a7880a99a1 New Crowdin translations by Github Action 2023-08-01 11:51:23 +01:00
Hariom Balhara a185861a06 chore: make Embed and AppStore Tests required (#10358)
* Now, all tests are required

* Add matrix support
2023-08-01 11:51:13 +01:00
Sean Brydon 6529d32b49 Rename button 2023-08-01 11:33:54 +01:00
Sean Brydon 587c788722 Add loader 2023-08-01 10:46:01 +01:00
Sean Brydon 9214873272 Refactor footer controls 2023-08-01 10:43:40 +01:00
Sean Brydon 35b4168eac Mutation Loading 2023-08-01 10:38:57 +01:00
Sean Brydon bf68100cdd FE/Add translations 2023-08-01 10:00:09 +01:00
Sean Brydon f7b8121c17 Add BE bulk delete users 2023-08-01 09:23:40 +01:00
Sean Brydon 77f924e5a5 Add auto accept 2023-07-31 15:50:45 +01:00
Sean Brydon 7a786c612d Optimistic updates 2023-07-31 15:43:27 +01:00
Sean Brydon d64e874dbe Use origin aware trigger 2023-07-31 15:37:22 +01:00
Sean Brydon 8dd61453b2 Add to teams! 2023-07-31 14:43:23 +01:00
Sean Brydon 4f5c71e95b Working set 2023-07-31 13:49:15 +01:00
Sean Brydon 521110ed15 Add shadow to teamlist 2023-07-31 13:43:32 +01:00
Sean Brydon 5625d1f347 Inital bulk team assignemnt 2023-07-31 11:40:33 +01:00
GitStart-Cal.com 3840e40e68 test: Create unit tests for react components in packages/ui/components/layout (#10458)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-31 09:16:56 +01:00
Neel Patel 5e4f365e92 fix: changes for showing error message in case of INTERNAL_SERVER_ERROR in event types (#10305)
* fix: changes for showing error message in case of INTERNAL_SERVER_ERROR

* changes for edge case

---------

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-07-31 09:16:56 +01:00
sean-brydon da23bd78a3 feat: Auto join team if already in org [CAL-2248] (#10450)
* Auto join if in org.

* Only handle accepted org memberships

* Add comment

* TODO: fix prisma mock
2023-07-31 09:16:56 +01:00
Udit Takkar d7800ac21e perf: reduce number of api calls in event types (#10381)
* perf: reduce number of api calls in event types

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

* chore

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

* perf: use staleTime and cacheTime

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-31 09:16:56 +01:00
Keith Williams 4bd2ea85f9 chore: Remove env file cache (#10297) 2023-07-31 09:16:56 +01:00
sean-brydon 74815727ba use cloudflare real IP (#10449) 2023-07-31 09:16:56 +01:00
Udit Takkar 7e8e9f5410 fix: use time format (#8787)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-31 09:16:56 +01:00
Crowdin Bot ac19bbe603 New Crowdin translations by Github Action 2023-07-31 09:16:56 +01:00
Udit Takkar c14356ecf9 fix: width and avatar in orgs app install dropdown (#10447) 2023-07-31 09:16:56 +01:00
Shivam Kalra 29b28bd803 chore: team booking test (#10440)
* feat: add team name in the createTeam fixture

* chore: remove unnecessary variables

* test: e2e booking for Round Robin EventType

* feat: teammates in user fixtures

* chore: DRY

* fix: remove redundant todo

* fix: add teamEvent title and slug in scenario

* test: booking title, RR hostname
2023-07-31 09:16:56 +01:00
Danila 84c0b6734a Add optimistic update for away status toggling (#10395)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-07-31 09:16:56 +01:00
GitStart-Cal.com ce2df3feec test: Create unit tests for react components in packages/ui/components/table (#10437)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-31 09:16:56 +01:00
GitStart-Cal.com b819f9ca12 test: Create unit tests for react components in packages/ui/components/form/colorpicker (#10426)
## DEMO

Run 
```
yarn test colorpicker
```
![image](https://github.com/calcom/cal.com/assets/121884634/31f52d82-615d-4926-bf44-0c1742a66526)
2023-07-31 09:16:56 +01:00
Crowdin Bot 4c0bfc8312 New Crowdin translations by Github Action 2023-07-31 09:16:56 +01:00
Peer Richelsen 44c885aeab fixed email embed illustraion (#10427) 2023-07-31 09:16:56 +01:00
nicktrn bbcbadbc4b fix: weekly limits (#10404)
* fix: correctly apply duration limits

* fix: weekly limits for weeks in two months

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-07-31 09:16:55 +01:00
Richard Poelderl 6f24a79760 chore: add datoCMS secret for handling webhooks (#10421)
* regen yarn.lock?

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

* Generated yarn.lock with submodules

* Added yarn.lock auth

* add datocms webhook secret

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-31 09:16:55 +01:00
Anik Dhabal Babu 1c282a2846 fix: Set the limit of Occurrences (#10269) 2023-07-31 09:16:55 +01:00
Cherish b2949e7c1d fix: Booking Confirmation Dialogbox (#10397)
Co-authored-by: cherish2003 <saicherissh90@gmail.com>
2023-07-31 09:16:55 +01:00
Alex van Andel d4a0a0af85 chore: Remove extra params from the users prop. (#10411)
* Removes extra params from the users prop.

* context no longer requires explicit type

* Remove user.avatar prop
2023-07-31 09:16:55 +01:00
Alex van Andel 47d1c78f76 Remove IP ban and tweak matcher accordingly (#10418) 2023-07-31 09:16:55 +01:00
Crowdin Bot d08fcc0eb6 New Crowdin translations by Github Action 2023-07-31 09:16:55 +01:00
sajanlamsal 801af91e53 fix: changed Japanese translations on booking page to better match the scenario. (#10416) 2023-07-31 09:16:55 +01:00
Crowdin Bot df7fffd369 New Crowdin translations by Github Action 2023-07-31 09:16:55 +01:00
GitStart-Cal.com 13c12bbfb8 test: Create unit tests for react components in packages/ui/components/TokenHandle (#10407)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-07-31 09:16:55 +01:00
GitStart-Cal.com 7131d9f8ee Update package.json (#10045)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-31 09:16:55 +01:00
GitStart-Cal.com 4d3e86330a test: Create unit tests for react components in packages/ui/components/form/checkbox (#10409)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-07-31 09:16:55 +01:00
Shivam Kalra bfcd193f6f test: Round Robin Team Booking (#10413) 2023-07-31 09:16:55 +01:00
Peer Richelsen 9a906f4d95 Update README.md 2023-07-31 09:16:55 +01:00
Crowdin Bot 714ecfb511 New Crowdin translations by Github Action 2023-07-31 09:16:55 +01:00
Hariom Balhara 8d0820be00 fix: middleware logic Routing Forms (#10412)
* Match /apps/routing_forms

* Next.js config.matcher doesnt support spread operator
2023-07-31 09:16:55 +01:00
Janakiram Yellapu 6e587d32ad feat: Add full names in the description of dynamic events. (#10251)
* Add full names in the descripiton of dynamic events.

* Initialising dynamic event desc after the check.

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

This reverts commit ddd89c4e25.

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

This reverts commit ce232d6256.

* Added booking title for dynamic events.

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

* Fix typo.

* Fix lint error.

* Fix redundant booking information.

* Fix type script lint.

* Addressed review suggestions.

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-31 09:16:55 +01:00
sean-brydon f978aef532 fix: ratelimit - updates (#10347) 2023-07-31 09:16:55 +01:00
Udit Takkar b3fa34f01b fix: email update bug (#10306)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-31 09:16:55 +01:00
sydwardrae 65c9b23a60 fix: spelling errors (#10376)
* fixed spelling errors

* chore: undo cancelled

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-07-31 09:16:55 +01:00
Hariom Balhara 1ace1f7d43 fix: Make routing-form a preinstalled app (#9836)
Co-authored-by: zomars <zomars@me.com>
2023-07-31 09:16:55 +01:00
Alex van Andel 224f9dafed v3.1.5 2023-07-31 09:16:55 +01:00
Leo Giovanetti f5b923968c chore: Skipping totp code for org dev (#10403) 2023-07-31 09:16:55 +01:00
Hariom Balhara 007a8d3da2 fix: Apps (#10394)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-07-31 09:16:55 +01:00
Leo Giovanetti 5ccea60719 fix: Missing subteam URL [CAL-2240] (#10399) 2023-07-31 09:16:55 +01:00
nicktrn 645cce6c3a fix: make husky install compatible with yarn v3 (#10393) 2023-07-31 09:16:55 +01:00
Udit Takkar 5a2fb9c506 fix: responsive upload avatar (#10392)
* fix: responsive upload avatar

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

* Update ImageUploader.tsx

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-31 09:16:55 +01:00
sean-brydon b9f1bdf283 fix: app.cal.com signup changes (#10369)
* Adds premium username check

* Refactor

* fix usernames

* Return a response so the request does not stall

* Fix conditional

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-31 09:16:55 +01:00
Anik Dhabal Babu 80d7973cfe fix: Toast Onclose is removing specific toast (#9960)
* toast behaviour fixed

* Update showToast.tsx

* Adjust E2E test for toast

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-07-31 09:16:55 +01:00
Crowdin Bot ca1e831836 New Crowdin translations by Github Action 2023-07-31 09:16:55 +01:00
sean-brydon 39f61d1757 fix: Feature flag user cache (#10359)
Co-authored-by: zomars <zomars@me.com>
2023-07-31 09:16:55 +01:00
Carina Wollendorfer 73ec25b511 add missing translation (#10380)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-31 09:16:55 +01:00
mohammed hussam 5766826efc feat: dynamic group links + flexible schedule (#9931)
* redirect user to /[user]/[type] for dynamic booking in /[user]

* feat: dynamic group links

* fix: type errors

* fix: more type errors

* Booker components cannot rely on useRouter

* Rename 15min/30/60 to dynamic

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

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-31 09:16:55 +01:00
Richard Poelderl 2d7bbb7951 chore: enable i18n-unused GH action in `website` (#10296)
* regen yarn.lock?

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

* Generated yarn.lock with submodules

* Added yarn.lock auth

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-07-31 09:16:55 +01:00
Udit Takkar d7d2641cfc fix: show event type setup tab in managed event types [CAL-2225] (#10367)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-07-31 09:16:55 +01:00
Pradumn Kumar 0aa7bec1b0 fix: Managed Event Type Webhooks (#10300)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-07-31 09:16:55 +01:00
Crowdin Bot 2734a0b2bd New Crowdin translations by Github Action 2023-07-31 09:16:55 +01:00
Leo Giovanetti 1f3427d91a fix: Username check for sign-up with invitation in org context (#10375) 2023-07-31 09:16:55 +01:00
Crowdin Bot 5af733ee2c New Crowdin translations by Github Action 2023-07-31 09:16:55 +01:00
Hariom Balhara 55808b22be chore: make Embed and AppStore Tests required (#10358)
* Now, all tests are required

* Add matrix support
2023-07-31 09:16:54 +01:00
Crowdin Bot 07bd24ffb0 New Crowdin translations by Github Action 2023-07-31 09:16:54 +01:00
Crowdin Bot 22854ca8f0 New Crowdin translations by Github Action 2023-07-31 09:16:54 +01:00
Keith Williams b95447c351 chore: Updated Docker release workflow to use environment variable (#10345) 2023-07-31 09:16:54 +01:00
Hariom Balhara daf3aa0a42 Fix booker top margin embed (#10357) 2023-07-31 09:16:54 +01:00
Sean Brydon 6fc3a9e1ca Update store 2023-07-28 15:10:26 +01:00
Sean Brydon 85c3354afd Add api to save 2023-07-28 15:01:27 +01:00
Sean Brydon 0540cf34d9 Fix spaces 2023-07-28 14:39:48 +01:00
Sean Brydon abc5db02c4 Add edit mode 2023-07-28 13:21:04 +01:00
Sean Brydon 9d39d20d37 Move components to seperate files 2023-07-28 09:46:14 +01:00
Sean Brydon f4a2bd54b4 Load teams -> basic ui / edit toggle 2023-07-26 15:18:27 +01:00
Sean Brydon 3a2f9d5eb6 Sheet - handle user - add tailwind-animate 2023-07-25 13:15:27 +01:00
32 changed files with 1211 additions and 155 deletions

View File

@ -167,6 +167,7 @@
"msw": "^0.42.3",
"postcss": "^8.4.18",
"tailwindcss": "^3.3.1",
"tailwindcss-animate": "^1.0.6",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
},

View File

@ -1256,6 +1256,7 @@
"error_updating_settings": "Error updating settings",
"personal_cal_url": "My personal {{appName}} URL",
"bio_hint": "A few sentences about yourself. this will appear on your personal url page.",
"user_has_no_bio": "This user has not added a bio yet.",
"delete_account_modal_title": "Delete Account",
"confirm_delete_account_modal": "Are you sure you want to delete your {{appName}} account?",
"delete_my_account": "Delete my account",
@ -1970,6 +1971,11 @@
"org_team_names_example_5": "e.g. Data Analytics Team",
"org_max_team_warnings": "You will be able to add more teams later on.",
"what_is_this_meeting_about": "What is this meeting about?",
"add_to_team":"Add to team",
"remove_users_from_org": "Remove users from organization",
"remove_users_from_org_confirm":"Are you sure you want to remove {{userCount}} users from this organization?",
"user_has_no_schedules":"This user has not setup any schedules yet",
"user_isnt_in_any_teams":"This user is not in any teams",
"requires_booker_email_verification": "Requires booker email verification",
"description_requires_booker_email_verification": "To ensure booker's email verification before scheduling events",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"

View File

@ -3,4 +3,5 @@ const base = require("@calcom/config/tailwind-preset");
module.exports = {
...base,
content: [...base.content, "../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}"],
plugins: [...base.plugins, require("tailwindcss-animate")],
};

View File

@ -3,12 +3,28 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["modules/*"],
"@components/*": ["components/*"],
"@lib/*": ["lib/*"],
"@server/*": ["server/*"],
"@prisma/client/*": ["@calcom/prisma/client/*"]
}
"~/*": [
"modules/*"
],
"@components/*": [
"components/*"
],
"@lib/*": [
"lib/*"
],
"@server/*": [
"server/*"
],
"@prisma/client/*": [
"@calcom/prisma/client/*"
]
},
"plugins": [
{
"name": "next"
}
],
"strictNullChecks": true
},
"include": [
/* Find a way to not require this - App files don't belong here. */
@ -17,7 +33,10 @@
"../../packages/types/*.d.ts",
"../../packages/types/next-auth.d.ts",
"**/*.ts",
"**/*.tsx"
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
"exclude": [
"node_modules"
]
}

View File

@ -15,6 +15,7 @@ export function BookFormAsModal({ visible, onCancel }: { visible: boolean; onCan
const selectedDuration = useBookerStore((state) => state.selectedDuration);
const { data } = useEvent();
const parsedSelectedTimeslot = dayjs(selectedTimeslot);
const { timeFormat, timezone } = useTimePreferences();
return (

View File

@ -0,0 +1,50 @@
import type { Table } from "@tanstack/react-table";
import { BanIcon } from "lucide-react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Dialog, DialogTrigger, ConfirmationDialogContent, Button, showToast } from "@calcom/ui";
import type { User } from "../UserListTable";
interface Props {
table: Table<User>;
}
export function DeleteBulkUsers({ table }: Props) {
const { t } = useLocale();
const selectedRows = table.getSelectedRowModel().flatRows.map((row) => row.original); // Get selected rows from table
const utils = trpc.useContext();
const deleteMutation = trpc.viewer.organizations.bulkDeleteUsers.useMutation({
onSuccess: () => {
utils.viewer.organizations.listMembers.invalidate();
showToast("Deleted Users", "success");
},
onError: (error) => {
showToast(error.message, "error");
},
});
return (
<Dialog>
<DialogTrigger asChild>
<Button StartIcon={BanIcon}>{t("Delete")}</Button>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("remove_users_from_org")}
confirmBtnText={t("remove")}
isLoading={deleteMutation.isLoading}
onConfirm={() => {
deleteMutation.mutateAsync({
userIds: selectedRows.map((user) => user.id),
});
}}>
<p className="mt-5">
{t("remove_users_from_org_confirm", {
userCount: selectedRows.length,
})}
</p>
</ConfirmationDialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,133 @@
import type { Table } from "@tanstack/react-table";
import { Users, Check } from "lucide-react";
import { useState } from "react";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import {
Command,
CommandInput,
CommandList,
CommandItem,
CommandEmpty,
CommandGroup,
Button,
Popover,
PopoverContent,
PopoverTrigger,
CommandSeparator,
showToast,
} from "@calcom/ui";
import type { User } from "../UserListTable";
interface Props {
table: Table<User>;
}
export function TeamListBulkAction({ table }: Props) {
const { data: teams } = trpc.viewer.organizations.getTeams.useQuery();
const [selectedValues, setSelectedValues] = useState<Set<number>>(new Set());
const utils = trpc.useContext();
const mutation = trpc.viewer.organizations.bulkAddToTeams.useMutation({
onError: (error) => {
showToast(error.message, "error");
},
onSuccess: (res) => {
showToast(
`${res.invitedTotalUsers} Users invited to ${Array.from(selectedValues).length} teams`,
"success"
);
// Optimistically update the data from query trpc cache listMembers
// We may need to set this data instread of invalidating. Will see how performance handles it
utils.viewer.organizations.listMembers.invalidate();
// Clear the selected values
setSelectedValues(new Set());
table.toggleAllRowsSelected(false);
},
});
const { t } = useLocale();
// Add a value to the set
const addValue = (value: number) => {
const updatedSet = new Set(selectedValues);
updatedSet.add(value);
setSelectedValues(updatedSet);
};
// Remove a value from the set
const removeValue = (value: number) => {
const updatedSet = new Set(selectedValues);
updatedSet.delete(value);
setSelectedValues(updatedSet);
};
return (
<>
<Popover>
<PopoverTrigger asChild>
<Button StartIcon={Users}>{t("add_to_team")}</Button>
</PopoverTrigger>
{/* We dont really use shadows much - but its needed here */}
<PopoverContent className="w-[200px] p-0 shadow-md" align="start" sideOffset={12}>
<Command>
<CommandInput placeholder={t("search")} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{teams &&
teams.map((option) => {
const isSelected = selectedValues.has(option.id);
return (
<CommandItem
key={option.id}
onSelect={() => {
console.log("onSelect", isSelected);
if (!isSelected) {
addValue(option.id);
} else {
removeValue(option.id);
}
}}>
<span>{option.name}</span>
<div
className={classNames(
"border-subtle ml-auto flex h-4 w-4 items-center justify-center rounded-sm border",
isSelected ? "text-emphasis" : "opacity-50 [&_svg]:invisible"
)}>
<Check className={classNames("h-4 w-4")} />
</div>
</CommandItem>
);
})}
</CommandGroup>
<>
<CommandSeparator />
<CommandGroup>
<div className="mb-1.5 flex w-full">
<Button
loading={mutation.isLoading}
className="ml-auto mr-1.5 rounded-md"
size="sm"
onClick={async () => {
const selectedRows = table.getSelectedRowModel().flatRows.map((row) => row.original);
mutation.mutateAsync({
userIds: selectedRows.map((row) => row.id),
teamIds: Array.from(selectedValues),
});
}}>
{t("apply")}
</Button>
</div>
</CommandGroup>
</>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</>
);
}

View File

@ -0,0 +1,70 @@
import { classNames } from "@calcom/lib";
import { useCopy } from "@calcom/lib/hooks/useCopy";
import type { BadgeProps } from "@calcom/ui";
import { Badge, Button, Label } from "@calcom/ui";
import { ClipboardCheck, Clipboard } from "@calcom/ui/components/icon";
type DisplayInfoType<T extends boolean> = {
label: string;
value: T extends true ? string[] : string;
asBadge?: boolean;
isArray?: T;
displayCopy?: boolean;
badgeColor?: BadgeProps["variant"];
} & (T extends false
? { displayCopy?: boolean; displayCount?: never }
: { displayCopy?: never; displayCount?: number }); // Only show displayCopy if its not an array is false
export function DisplayInfo<T extends boolean>({
label,
value,
asBadge,
isArray,
displayCopy,
displayCount,
badgeColor,
}: DisplayInfoType<T>) {
const { copyToClipboard, isCopied } = useCopy();
const values = (isArray ? value : [value]) as string[];
return (
<div className="flex flex-col">
<Label className="text-subtle mb-1 text-xs font-semibold uppercase leading-none">
{label} {displayCount && `(${displayCount})`}
</Label>
<div className={classNames(asBadge ? "mt-0.5 flex space-x-2" : "flex flex-col")}>
<>
{values.map((v) => {
const content = (
<span
className={classNames(
"text-emphasis inline-flex items-center gap-1 font-normal leading-5",
asBadge ? "text-xs" : "text-sm"
)}>
{v}
{displayCopy && (
<Button
size="sm"
variant="icon"
onClick={() => copyToClipboard(v)}
color="minimal"
className="text-subtle rounded-md"
StartIcon={isCopied ? ClipboardCheck : Clipboard}
/>
)}
</span>
);
return asBadge ? (
<Badge variant={badgeColor} size="sm">
{content}
</Badge>
) : (
content
);
})}
</>
</div>
</div>
);
}

View File

@ -0,0 +1,249 @@
import { zodResolver } from "@hookform/resolvers/zod";
import type { Dispatch } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { shallow } from "zustand/shallow";
import { useOrgBranding } from "@calcom/ee/organizations/context/provider";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import {
Sheet,
SheetContent,
SheetFooter,
Avatar,
Skeleton,
Form,
TextField,
ToggleGroup,
TextAreaField,
TimezoneSelect,
Label,
showToast,
Loader,
} from "@calcom/ui";
import type { State, Action } from "../UserListTable";
import { DisplayInfo } from "./DisplayInfo";
import { SheetFooterControls } from "./SheetFooterControls";
import { useEditMode } from "./store";
const editSchema = z.object({
name: z.string(),
email: z.string().email(),
bio: z.string(),
role: z.enum(["ADMIN", "MEMBER"]),
timeZone: z.string(),
// schedules: z.array(z.string()),
// teams: z.array(z.string()),
});
type EditSchema = z.infer<typeof editSchema>;
function EditForm({
selectedUser,
avatarUrl,
domainUrl,
dispatch,
}: {
selectedUser: RouterOutputs["viewer"]["organizations"]["getUser"];
avatarUrl: string;
domainUrl: string;
dispatch: Dispatch<Action>;
}) {
const [setMutationLoading] = useEditMode((state) => [state.setMutationloading], shallow);
const { t } = useLocale();
const utils = trpc.useContext();
const form = useForm({
resolver: zodResolver(editSchema),
defaultValues: {
name: selectedUser?.name ?? "",
email: selectedUser?.email ?? "",
bio: selectedUser?.bio ?? "",
role: selectedUser?.role ?? "",
timeZone: selectedUser?.timeZone ?? "",
},
});
const mutation = trpc.viewer.organizations.updateUser.useMutation({
onSuccess: () => {
dispatch({ type: "CLOSE_MODAL" });
utils.viewer.organizations.listMembers.invalidate();
showToast(t("profile_updated_successfully"), "success");
},
onError: (error) => {
showToast(error.message, "error");
},
onSettled: () => {
/**
* /We need to do this as the submit button lives out side
* the form for some complicated reason so we can't relay on mutationState
*/
setMutationLoading(false);
},
});
const watchTimezone = form.watch("timeZone");
return (
<Form
form={form}
id="edit-user-form"
handleSubmit={(values) => {
setMutationLoading(true);
mutation.mutate({
userId: selectedUser?.id ?? "",
role: values.role as "ADMIN" | "MEMBER", // Cast needed as we dont provide an option for owner
name: values.name,
email: values.email,
bio: values.bio,
timeZone: values.timeZone,
});
}}>
<div className="mt-4 flex items-center gap-2">
<Avatar
size="lg"
alt={`${selectedUser?.name} avatar`}
imageSrc={avatarUrl}
gravatarFallbackMd5="fallback"
/>
<div className="space-between flex flex-col leading-none">
<span className="text-emphasis text-lg font-semibold">{selectedUser?.name ?? "Nameless User"}</span>
<p className="subtle text-sm font-normal">
{domainUrl}/{selectedUser?.username}
</p>
</div>
</div>
<div className="mt-6 flex flex-col space-y-3">
<TextField label={t("name")} {...form.register("name")} />
<TextField label={t("email")} {...form.register("email")} />
<TextAreaField label={t("bio")} {...form.register("bio")} className="min-h-52" />
<div>
<Label>{t("role")}</Label>
<ToggleGroup
isFullWidth
defaultValue={selectedUser?.role ?? "MEMBER"}
value={form.watch("role")}
options={[
{
value: "MEMBER",
label: t("member"),
},
{
value: "ADMIN",
label: t("admin"),
},
]}
onValueChange={(value: EditSchema["role"]) => {
form.setValue("role", value);
}}
/>
</div>
<div>
<Label>{t("timezone")}</Label>
<TimezoneSelect value={watchTimezone ?? "America/Los_Angeles"} />
</div>
</div>
</Form>
);
}
export function EditUserSheet({ state, dispatch }: { state: State; dispatch: Dispatch<Action> }) {
const { t } = useLocale();
const { user: selectedUser } = state.editSheet;
const orgBranding = useOrgBranding();
const [editMode, setEditMode] = useEditMode((state) => [state.editMode, state.setEditMode], shallow);
const { data: loadedUser, isLoading } = trpc.viewer.organizations.getUser.useQuery({
userId: selectedUser?.id,
});
const avatarURL = `${orgBranding?.fullDomain ?? WEBAPP_URL}/${loadedUser?.username}/avatar.png`;
const schedulesNames = loadedUser?.schedules && loadedUser?.schedules.map((s) => s.name);
const teamNames =
loadedUser?.teams && loadedUser?.teams.map((t) => `${t.name} ${!t.accepted ? "(pending)" : ""}`);
return (
<Sheet
open={true}
onOpenChange={() => {
setEditMode(false);
dispatch({ type: "CLOSE_MODAL" });
}}>
<SheetContent position="right" size="default">
{!isLoading && loadedUser ? (
<div className="flex h-full flex-col">
{!editMode ? (
<div className="flex-grow">
<div className="mt-4 flex items-center gap-2">
<Avatar
asChild
className="h-[36px] w-[36px]"
alt={`${loadedUser?.name} avatar`}
imageSrc={avatarURL}
gravatarFallbackMd5="fallback"
/>
<div className="space-between flex flex-col leading-none">
<Skeleton loading={isLoading} as="p" waitForTranslation={false}>
<span className="text-emphasis text-lg font-semibold">
{loadedUser?.name ?? "Nameless User"}
</span>
</Skeleton>
<Skeleton loading={isLoading} as="p" waitForTranslation={false}>
<p className="subtle text-sm font-normal">
{orgBranding?.fullDomain ?? WEBAPP_URL}/{loadedUser?.username}
</p>
</Skeleton>
</div>
</div>
<div className="mt-6 flex flex-col space-y-5">
<DisplayInfo label={t("email")} value={loadedUser?.email ?? ""} displayCopy />
<DisplayInfo
label={t("bio")}
badgeColor="gray"
value={loadedUser?.bio ? loadedUser?.bio : t("user_has_no_bio")}
/>
<DisplayInfo label={t("role")} value={loadedUser?.role ?? ""} asBadge badgeColor="blue" />
<DisplayInfo label={t("timezone")} value={loadedUser?.timeZone ?? ""} />
<DisplayInfo
label={t("availability_schedules")}
value={
schedulesNames && schedulesNames?.length === 0
? [t("user_has_no_schedules")]
: schedulesNames ?? "" // TS wtf
}
/>
<DisplayInfo
label={t("teams")}
displayCount={teamNames?.length ?? 0}
value={
teamNames && teamNames?.length === 0 ? [t("user_isnt_in_any_teams")] : teamNames ?? "" // TS wtf
}
asBadge={teamNames && teamNames?.length > 0}
/>
</div>
</div>
) : (
<div className="flex-grow">
<EditForm
selectedUser={loadedUser}
avatarUrl={avatarURL}
domainUrl={orgBranding?.fullDomain ?? WEBAPP_URL}
dispatch={dispatch}
/>
</div>
)}
<SheetFooter className="mt-auto">
<SheetFooterControls />
</SheetFooter>
</div>
) : (
<Loader />
)}
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,60 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SheetClose, Button } from "@calcom/ui";
import { Pencil } from "@calcom/ui/components/icon";
import { useEditMode } from "./store";
function EditModeFooter() {
const { t } = useLocale();
const setEditMode = useEditMode((state) => state.setEditMode);
const isLoading = useEditMode((state) => state.mutationLoading);
return (
<>
<Button
color="secondary"
type="button"
className="justify-center md:w-1/5"
onClick={() => {
setEditMode(false);
}}>
{t("cancel")}
</Button>
<Button type="submit" className="w-full justify-center" form="edit-user-form" loading={isLoading}>
{t("update")}
</Button>
</>
);
}
function MoreInfoFooter() {
const { t } = useLocale();
const setEditMode = useEditMode((state) => state.setEditMode);
return (
<>
<SheetClose asChild>
<Button color="secondary" type="button" className="justify-center md:w-1/5">
{t("close")}
</Button>
</SheetClose>
<Button
type="button"
onClick={() => {
setEditMode(true);
}}
className="w-full justify-center gap-2"
variant="icon"
key="EDIT_BUTTON"
StartIcon={Pencil}>
{t("edit")}
</Button>
</>
);
}
export function SheetFooterControls() {
const editMode = useEditMode((state) => state.editMode);
return <>{editMode ? <EditModeFooter /> : <MoreInfoFooter />}</>;
}

View File

@ -0,0 +1,15 @@
import { create } from "zustand";
interface EditModeState {
editMode: boolean;
setEditMode: (editMode: boolean) => void;
mutationLoading: boolean;
setMutationloading: (loading: boolean) => void;
}
export const useEditMode = create<EditModeState>((set) => ({
editMode: false,
setEditMode: (editMode) => set({ editMode }),
mutationLoading: false,
setMutationloading: (loading) => set({ mutationLoading: loading }),
}));

View File

@ -1,5 +1,5 @@
import type { ColumnDef } from "@tanstack/react-table";
import { Plus, StopCircle, Users } from "lucide-react";
import { Plus } from "lucide-react";
import { useSession } from "next-auth/react";
import { useMemo, useRef, useCallback, useEffect, useReducer } from "react";
@ -7,11 +7,14 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc";
import { Avatar, Badge, Button, DataTable } from "@calcom/ui";
import { Avatar, Badge, Button, DataTable, Checkbox } from "@calcom/ui";
import { useOrgBranding } from "../../../ee/organizations/context/provider";
import { DeleteBulkUsers } from "./BulkActions/DeleteBulkUsers";
import { TeamListBulkAction } from "./BulkActions/TeamList";
import { ChangeUserRoleModal } from "./ChangeUserRoleModal";
import { DeleteMemberModal } from "./DeleteMemberModal";
import { EditUserSheet } from "./EditSheet/EditUserSheet";
import { ImpersonationMemberModal } from "./ImpersonationMemberModal";
import { InviteMemberModal } from "./InviteMemberModal";
import { TableActions } from "./UserTableActions";
@ -42,11 +45,17 @@ export type State = {
deleteMember: Payload;
impersonateMember: Payload;
inviteMember: Payload;
editSheet: Payload;
};
export type Action =
| {
type: "SET_CHANGE_MEMBER_ROLE_ID" | "SET_DELETE_ID" | "SET_IMPERSONATE_ID" | "INVITE_MEMBER";
type:
| "SET_CHANGE_MEMBER_ROLE_ID"
| "SET_DELETE_ID"
| "SET_IMPERSONATE_ID"
| "INVITE_MEMBER"
| "EDIT_USER_SHEET";
payload: Payload;
}
| {
@ -66,6 +75,9 @@ const initialState: State = {
inviteMember: {
showModal: false,
},
editSheet: {
showModal: false,
},
};
function reducer(state: State, action: Action): State {
@ -78,6 +90,8 @@ function reducer(state: State, action: Action): State {
return { ...state, impersonateMember: action.payload };
case "INVITE_MEMBER":
return { ...state, inviteMember: action.payload };
case "EDIT_USER_SHEET":
return { ...state, editSheet: action.payload };
case "CLOSE_MODAL":
return {
...state,
@ -85,6 +99,7 @@ function reducer(state: State, action: Action): State {
deleteMember: { showModal: false },
impersonateMember: { showModal: false },
inviteMember: { showModal: false },
editSheet: { showModal: false },
};
default:
return state;
@ -121,25 +136,25 @@ export function UserListTable() {
};
const cols: ColumnDef<User>[] = [
// Disabling select for this PR: Will work on actions etc in a follow up
// {
// id: "select",
// header: ({ table }) => (
// <Checkbox
// checked={table.getIsAllPageRowsSelected()}
// onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
// aria-label="Select all"
// className="translate-y-[2px]"
// />
// ),
// cell: ({ row }) => (
// <Checkbox
// checked={row.getIsSelected()}
// onCheckedChange={(value) => row.toggleSelected(!!value)}
// aria-label="Select row"
// className="translate-y-[2px]"
// />
// ),
// },
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
className="translate-y-[2px]"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
className="translate-y-[2px]"
/>
),
},
{
id: "member",
accessorFn: (data) => data.email,
@ -269,18 +284,12 @@ export function UserListTable() {
searchKey="member"
selectionOptions={[
{
label: "Add To Team",
onClick: () => {
console.log("Add To Team");
},
icon: Users,
type: "render",
render: (table) => <TeamListBulkAction table={table} />,
},
{
label: "Delete",
onClick: () => {
console.log("Delete");
},
icon: StopCircle,
type: "render",
render: (table) => <DeleteBulkUsers table={table} />,
},
]}
tableContainerRef={tableContainerRef}
@ -326,6 +335,7 @@ export function UserListTable() {
{state.inviteMember.showModal && <InviteMemberModal dispatch={dispatch} />}
{state.impersonateMember.showModal && <ImpersonationMemberModal dispatch={dispatch} state={state} />}
{state.changeMemberRole.showModal && <ChangeUserRoleModal dispatch={dispatch} state={state} />}
{state.editSheet.showModal && <EditUserSheet dispatch={dispatch} state={state} />}
</>
);
}

View File

@ -66,7 +66,7 @@ export function TableActions({
type="button"
onClick={() =>
dispatch({
type: "SET_CHANGE_MEMBER_ROLE_ID",
type: "EDIT_USER_SHEET",
payload: {
user,
showModal: true,
@ -140,7 +140,7 @@ export function TableActions({
type="button"
onClick={() =>
dispatch({
type: "SET_IMPERSONATE_ID",
type: "EDIT_USER_SHEET",
payload: {
user,
showModal: true,

View File

@ -0,0 +1,27 @@
import { useState, useEffect } from "react";
export function useCopy() {
const [isCopied, setIsCopied] = useState(false);
const copyToClipboard = (text: string) => {
if (typeof navigator !== "undefined" && navigator.clipboard) {
navigator.clipboard
.writeText(text)
.then(() => setIsCopied(true))
.catch((error) => console.error("Copy to clipboard failed:", error));
}
};
const resetCopyStatus = () => {
setIsCopied(false);
};
useEffect(() => {
if (isCopied) {
const timer = setTimeout(resetCopyStatus, 3000); // Reset copy status after 3 seconds
return () => clearTimeout(timer);
}
}, [isCopied]);
return { isCopied, copyToClipboard, resetCopyStatus };
}

View File

@ -2,13 +2,17 @@ import { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
import authedProcedure, { authedAdminProcedure } from "../../../procedures/authedProcedure";
import { router } from "../../../trpc";
import { ZAddBulkTeams } from "./addBulkTeams.schema";
import { ZAdminVerifyInput } from "./adminVerify.schema";
import { ZBulkUsersDelete } from "./bulkDeleteUsers.schema.";
import { ZCreateInputSchema } from "./create.schema";
import { ZCreateTeamsSchema } from "./createTeams.schema";
import { ZGetMembersInput } from "./getMembers.schema";
import { ZGetUserInput } from "./getUser.schema";
import { ZListMembersSchema } from "./listMembers.schema";
import { ZSetPasswordSchema } from "./setPassword.schema";
import { ZUpdateInputSchema } from "./update.schema";
import { ZUpdateUserInputSchema } from "./updateUser.schema";
type OrganizationsRouterHandlerCache = {
create?: typeof import("./create.handler").createHandler;
@ -24,6 +28,11 @@ type OrganizationsRouterHandlerCache = {
listMembers?: typeof import("./listMembers.handler").listMembersHandler;
getBrand?: typeof import("./getBrand.handler").getBrandHandler;
getMembers?: typeof import("./getMembers.handler").getMembersHandler;
getUser?: typeof import("./getUser.handler").getUserHandler;
updateUser?: typeof import("./updateUser.handler").updateUserHandler;
getTeams?: typeof import("./getTeams.handler").getTeamsHandler;
bulkAddToTeams?: typeof import("./addBulkTeams.handler").addBulkTeamsHandler;
bulkDeleteUsers?: typeof import("./bulkDeleteUsers.handler").bulkDeleteUsersHandler;
};
const UNSTABLE_HANDLER_CACHE: OrganizationsRouterHandlerCache = {};
@ -227,4 +236,84 @@ export const viewerOrganizationsRouter = router({
ctx,
});
}),
getUser: authedProcedure.input(ZGetUserInput).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.getUser) {
UNSTABLE_HANDLER_CACHE.getUser = await import("./getUser.handler").then((mod) => mod.getUserHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.getUser) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.getUser({
ctx,
input,
});
}),
updateUser: authedProcedure.input(ZUpdateUserInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.updateUser) {
UNSTABLE_HANDLER_CACHE.updateUser = await import("./updateUser.handler").then(
(mod) => mod.updateUserHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.updateUser) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.updateUser({
ctx,
input,
});
}),
getTeams: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.getTeams) {
UNSTABLE_HANDLER_CACHE.getTeams = await import("./getTeams.handler").then((mod) => mod.getTeamsHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.getTeams) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.getTeams({
ctx,
});
}),
bulkAddToTeams: authedProcedure.input(ZAddBulkTeams).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.bulkAddToTeams) {
UNSTABLE_HANDLER_CACHE.bulkAddToTeams = await import("./addBulkTeams.handler").then(
(mod) => mod.addBulkTeamsHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.bulkAddToTeams) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.bulkAddToTeams({
ctx,
input,
});
}),
bulkDeleteUsers: authedProcedure.input(ZBulkUsersDelete).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.bulkDeleteUsers) {
UNSTABLE_HANDLER_CACHE.bulkDeleteUsers = await import("./bulkDeleteUsers.handler").then(
(mod) => mod.bulkDeleteUsersHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.bulkDeleteUsers) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.bulkDeleteUsers({
ctx,
input,
});
}),
});

View File

@ -0,0 +1,86 @@
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../trpc";
import type { TAddBulkTeams } from "./addBulkTeams.schema";
type AddBulkTeamsHandler = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TAddBulkTeams;
};
export async function addBulkTeamsHandler({ ctx, input }: AddBulkTeamsHandler) {
const currentUser = ctx.user;
if (!currentUser.organizationId) throw new TRPCError({ code: "UNAUTHORIZED" });
// check if user is admin of organization
if (!(await isOrganisationAdmin(currentUser?.id, currentUser.organizationId)))
throw new TRPCError({ code: "UNAUTHORIZED" });
// Loop over all users and check if they are already in the organization
const usersInOrganization = await prisma.membership.findMany({
where: {
user: {
organizationId: currentUser.organizationId,
id: {
in: input.userIds,
},
},
accepted: true,
},
distinct: ["userId"],
});
// Throw error if any of the users are not in the organization. They should be invited to the organization via the onboaring flow first.
if (usersInOrganization.length !== input.userIds.length) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "One or more users are not in the organization",
});
}
// loop over all users and check if they are already in team they are being invited to
const usersInTeams = await prisma.membership.findMany({
where: {
userId: {
in: input.userIds,
},
teamId: {
in: input.teamIds,
},
},
});
// Filter out users who are already in teams they are being invited to
const filteredUserIds = input.userIds.filter((userId) => {
return !usersInTeams.some((membership) => membership.userId === userId);
});
// Loop over all users and add them to all teams in the array
const membershipData = filteredUserIds.flatMap((userId) =>
input.teamIds.map((teamId) => {
return {
userId,
teamId,
role: MembershipRole.MEMBER,
accepted: true, // Auto accept cause we know at this point they are already in the organization
} as Prisma.MembershipCreateManyInput;
})
);
await prisma.membership.createMany({
data: membershipData,
});
return {
success: true,
invitedTotalUsers: input.userIds.length,
};
}

View File

@ -0,0 +1,8 @@
import { z } from "zod";
export const ZAddBulkTeams = z.object({
userIds: z.array(z.number()),
teamIds: z.array(z.number()),
});
export type TAddBulkTeams = z.infer<typeof ZAddBulkTeams>;

View File

@ -0,0 +1,60 @@
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import { prisma } from "@calcom/prisma";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../trpc";
import type { TBulkUsersDelete } from "./bulkDeleteUsers.schema.";
type BulkDeleteUsersHandler = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TBulkUsersDelete;
};
export async function bulkDeleteUsersHandler({ ctx, input }: BulkDeleteUsersHandler) {
const currentUser = ctx.user;
if (!currentUser.organizationId) throw new TRPCError({ code: "UNAUTHORIZED" });
// check if user is admin of organization
if (!(await isOrganisationAdmin(currentUser?.id, currentUser.organizationId)))
throw new TRPCError({ code: "UNAUTHORIZED" });
// Loop over all users in input.userIds and remove all memberships for the organization including child teams
const deleteMany = prisma.membership.deleteMany({
where: {
userId: {
in: input.userIds,
},
team: {
OR: [
{
parentId: currentUser.organizationId,
},
{ id: currentUser.organizationId },
],
},
},
});
const removeOrgrelation = prisma.user.updateMany({
where: {
id: {
in: input.userIds,
},
},
data: {
organizationId: null,
},
});
// We do this in a transaction to make sure that all memberships are removed before we remove the organization relation from the user
// We also do this to make sure that if one of the queries fail, the whole transaction fails
await prisma.$transaction([deleteMany, removeOrgrelation]);
return {
success: true,
usersDeleted: input.userIds.length,
};
}

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZBulkUsersDelete = z.object({
userIds: z.array(z.number()),
});
export type TBulkUsersDelete = z.infer<typeof ZBulkUsersDelete>;

View File

@ -0,0 +1,34 @@
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import { prisma } from "@calcom/prisma";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../trpc";
type GetTeamsHandler = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export async function getTeamsHandler({ ctx }: GetTeamsHandler) {
const currentUser = ctx.user;
if (!currentUser.organizationId) throw new TRPCError({ code: "UNAUTHORIZED" });
// check if user is admin of organization
if (!(await isOrganisationAdmin(currentUser?.id, currentUser.organizationId)))
throw new TRPCError({ code: "UNAUTHORIZED" });
const allOrgTeams = await prisma.team.findMany({
where: {
parentId: currentUser.organizationId,
},
select: {
id: true,
name: true,
},
});
return allOrgTeams;
}

View File

@ -0,0 +1,87 @@
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import { prisma } from "@calcom/prisma";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../trpc";
import type { TGetUserInput } from "./getUser.schema";
type AdminVerifyOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TGetUserInput;
};
export async function getUserHandler({ input, ctx }: AdminVerifyOptions) {
const currentUser = ctx.user;
if (!currentUser.organizationId) throw new TRPCError({ code: "UNAUTHORIZED" });
// check if user is admin of organization
if (!(await isOrganisationAdmin(currentUser?.id, currentUser.organizationId)))
throw new TRPCError({ code: "UNAUTHORIZED" });
// get requested user from database and ensure they are in the same organization
const [requestedUser, membership, teams] = await prisma.$transaction([
prisma.user.findFirst({
where: { id: input.userId, organizationId: currentUser.organizationId },
select: {
id: true,
email: true,
username: true,
name: true,
bio: true,
timeZone: true,
schedules: {
select: {
id: true,
name: true,
},
},
},
}),
// Query on accepted as we don't want the user to be able to get this much info on a user that hasn't accepted the invite
prisma.membership.findFirst({
where: {
userId: input.userId,
teamId: currentUser.organizationId,
accepted: true,
},
select: {
role: true,
},
}),
prisma.membership.findMany({
where: {
userId: input.userId,
team: {
parentId: currentUser.organizationId,
},
},
select: {
team: {
select: {
id: true,
name: true,
},
},
accepted: true,
},
}),
]);
if (!requestedUser || !membership)
throw new TRPCError({ code: "UNAUTHORIZED", message: "user_not_exist_or_not_in_org" });
const foundUser = {
...requestedUser,
teams: teams.map((team) => ({
...team.team,
accepted: team.accepted,
})),
role: membership.role,
};
return foundUser;
}

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZGetUserInput = z.object({
userId: z.number().optional(),
});
export type TGetUserInput = z.infer<typeof ZGetUserInput>;

View File

@ -0,0 +1,67 @@
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TUpdateUserInputSchema } from "./updateUser.schema";
type UpdateUserOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TUpdateUserInputSchema;
};
export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
const { user } = ctx;
const { id: userId, organizationId } = user;
if (!organizationId)
throw new TRPCError({ code: "UNAUTHORIZED", message: "You must be a memeber of an organizaiton" });
if (!(await isOrganisationAdmin(userId, organizationId))) throw new TRPCError({ code: "UNAUTHORIZED" });
// Is requested user a member of the organization?
const requestedMember = await prisma.membership.findFirst({
where: {
userId: input.userId,
teamId: organizationId,
accepted: true,
},
});
if (!requestedMember)
throw new TRPCError({ code: "UNAUTHORIZED", message: "User does not belong to your organization" });
// Update user
await prisma.$transaction([
prisma.user.update({
where: {
id: input.userId,
},
data: {
bio: input.bio,
email: input.email,
name: input.name,
timeZone: input.timeZone,
},
}),
prisma.membership.update({
where: {
userId_teamId: {
userId: input.userId,
teamId: organizationId,
},
},
data: {
role: input.role,
},
}),
]);
// TODO: audit log this
return {
success: true,
};
};

View File

@ -0,0 +1,12 @@
import { z } from "zod";
export const ZUpdateUserInputSchema = z.object({
userId: z.number(),
bio: z.string().optional(),
name: z.string().optional(),
email: z.string().optional(),
role: z.enum(["ADMIN", "MEMBER"]),
timeZone: z.string(),
});
export type TUpdateUserInputSchema = z.infer<typeof ZUpdateUserInputSchema>;

View File

@ -12,7 +12,7 @@ import { Tooltip } from "../tooltip";
export type AvatarProps = {
className?: string;
size: "xxs" | "xs" | "xsm" | "sm" | "md" | "mdLg" | "lg" | "xl";
size?: "xxs" | "xs" | "xsm" | "sm" | "md" | "mdLg" | "lg" | "xl";
imageSrc?: Maybe<string>;
title?: string;
alt: string;
@ -35,7 +35,7 @@ const sizesPropsBySize = {
} as const;
export function Avatar(props: AvatarProps) {
const { imageSrc, gravatarFallbackMd5, size, alt, title, href } = props;
const { imageSrc, gravatarFallbackMd5, size = "md", alt, title, href } = props;
const rootClass = classNames("aspect-square rounded-full", sizesPropsBySize[size]);
let avatar = (
<AvatarPrimitive.Root

View File

@ -95,7 +95,7 @@ const CommandSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={classNames("bg-border -mx-1 h-px", className)}
className={classNames("bg-subtle -mx-1 mb-2 h-px", className)}
{...props}
/>
));

View File

@ -1,16 +1,25 @@
import type { Table } from "@tanstack/react-table";
import { Fragment } from "react";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import { Button } from "../button";
export type ActionItem<TData> =
| {
type: "action";
label: string;
onClick: () => void;
icon?: SVGComponent;
}
| {
type: "render";
render: (table: Table<TData>) => React.ReactNode;
};
interface DataTableSelectionBarProps<TData> {
table: Table<TData>;
actions?: {
label: string;
onClick: () => void;
icon?: SVGComponent;
}[];
actions?: ActionItem<TData>[];
}
export function DataTableSelectionBar<TData>({ table, actions }: DataTableSelectionBarProps<TData>) {
@ -21,10 +30,16 @@ export function DataTableSelectionBar<TData>({ table, actions }: DataTableSelect
return (
<div className="bg-brand-default text-brand item-center absolute bottom-0 left-1/2 flex -translate-x-1/2 gap-4 rounded-lg p-2">
<div className="text-brand-subtle my-auto px-2">{numberOfSelectedRows} selected</div>
{actions?.map((action) => (
<Button aria-label={action.label} onClick={action.onClick} StartIcon={action.icon} key={action.label}>
{action.label}
</Button>
{actions?.map((action, index) => (
<Fragment key={index}>
{action.type === "action" ? (
<Button aria-label={action.label} onClick={action.onClick} StartIcon={action.icon}>
{action.label}
</Button>
) : action.type === "render" ? (
action.render(table)
) : null}
</Fragment>
))}
</div>
);

View File

@ -17,9 +17,8 @@ import {
import { useState } from "react";
import { useVirtual } from "react-virtual";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../table/TableNew";
import type { ActionItem } from "./DataTableSelectionBar";
import { DataTableSelectionBar } from "./DataTableSelectionBar";
import type { FilterableItems } from "./DataTableToolbar";
import { DataTableToolbar } from "./DataTableToolbar";
@ -30,11 +29,7 @@ export interface DataTableProps<TData, TValue> {
data: TData[];
searchKey?: string;
filterableItems?: FilterableItems;
selectionOptions?: {
label: string;
onClick: () => void;
icon?: SVGComponent;
}[];
selectionOptions?: ActionItem<TData>[];
tableCTA?: React.ReactNode;
isLoading?: boolean;
onScroll?: (e: React.UIEvent<HTMLDivElement, UIEvent>) => void;

View File

@ -52,10 +52,10 @@ const sheetVariants = cva(
{
variants: {
position: {
top: "animate-in slide-in-from-top w-full duration-300",
bottom: "animate-in slide-in-from-bottom w-full duration-300",
left: "animate-in slide-in-from-left h-full duration-300",
right: "animate-in slide-in-from-right h-full duration-300",
top: "animate-in slide-in-from-top w-full duration-200",
bottom: "animate-in slide-in-from-bottom w-full duration-200",
left: "animate-in slide-in-from-left h-full duration-200",
right: "animate-in slide-in-from-right h-full duration-200",
},
size: {
content: "",
@ -105,12 +105,12 @@ const sheetVariants = cva(
{
position: ["right", "left"],
size: "default",
class: "w-1/3 h-[calc(100vh-2rem)]",
class: "w-1/3 max-h-[calc(100vh-2rem)]",
},
{
position: ["right", "left"],
size: "sm",
class: "w-1/4 h-[calc(100vh-2rem)]",
class: "w-1/4 max-h-[calc(100vh-2rem)]",
},
{
position: ["right", "left"],

View File

@ -36,7 +36,7 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
({ className, ...props }, ref) => (
<tr
ref={ref}
className={classNames("hover:bg-subtle data-[state=selected]:bg-subtle border-b", className)}
className={classNames("hover:bg-subtle data-[state=selected]:bg-muted border-b", className)}
{...props}
/>
)

View File

@ -138,3 +138,26 @@ export { useCalcomTheme } from "./styles/useCalcomTheme";
export { ScrollableArea } from "./components/scrollable/ScrollableArea";
export { WizardLayout } from "./layouts/WizardLayout";
export { DataTable } from "./components/data-table";
export {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "./components/sheet/sheet";
export {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
CommandInput,
CommandSeparator,
CommandShortcut,
} from "./components/command";
export { Popover, PopoverContent, PopoverTrigger } from "./components/popover";

View File

@ -131,25 +131,6 @@ __metadata:
languageName: node
linkType: hard
"@auth/core@npm:^0.1.4":
version: 0.1.4
resolution: "@auth/core@npm:0.1.4"
dependencies:
"@panva/hkdf": 1.0.2
cookie: 0.5.0
jose: 4.11.1
oauth4webapi: 2.0.5
preact: 10.11.3
preact-render-to-string: 5.2.3
peerDependencies:
nodemailer: 6.8.0
peerDependenciesMeta:
nodemailer:
optional: true
checksum: 64854404ea1883e0deb5535b34bed95cd43fc85094aeaf4f15a79e14045020eb944f844defe857edfc8528a0a024be89cbb2a3069dedef0e9217a74ca6c3eb79
languageName: node
linkType: hard
"@aws-crypto/ie11-detection@npm:^3.0.0":
version: 3.0.0
resolution: "@aws-crypto/ie11-detection@npm:3.0.0"
@ -3851,41 +3832,6 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/auth@workspace:apps/auth":
version: 0.0.0-use.local
resolution: "@calcom/auth@workspace:apps/auth"
dependencies:
"@auth/core": ^0.1.4
"@calcom/app-store": "*"
"@calcom/app-store-cli": "*"
"@calcom/config": "*"
"@calcom/core": "*"
"@calcom/dayjs": "*"
"@calcom/embed-core": "workspace:*"
"@calcom/embed-react": "workspace:*"
"@calcom/embed-snippet": "workspace:*"
"@calcom/features": "*"
"@calcom/lib": "*"
"@calcom/prisma": "*"
"@calcom/trpc": "*"
"@calcom/tsconfig": "*"
"@calcom/types": "*"
"@calcom/ui": "*"
"@types/node": 16.9.1
"@types/react": 18.0.26
"@types/react-dom": ^18.0.9
eslint: ^8.34.0
eslint-config-next: ^13.2.1
next: ^13.4.6
next-auth: ^4.22.1
postcss: ^8.4.18
react: ^18.2.0
react-dom: ^18.2.0
tailwindcss: ^3.3.1
typescript: ^4.9.4
languageName: unknown
linkType: soft
"@calcom/caldavcalendar@workspace:packages/app-store/caldavcalendar":
version: 0.0.0-use.local
resolution: "@calcom/caldavcalendar@workspace:packages/app-store/caldavcalendar"
@ -4880,6 +4826,7 @@ __metadata:
stripe: ^9.16.0
superjson: 1.9.1
tailwindcss: ^3.3.1
tailwindcss-animate: ^1.0.6
tailwindcss-radix: ^2.6.0
ts-node: ^10.9.1
turndown: ^7.1.1
@ -7659,13 +7606,6 @@ __metadata:
languageName: node
linkType: hard
"@panva/hkdf@npm:1.0.2":
version: 1.0.2
resolution: "@panva/hkdf@npm:1.0.2"
checksum: 75183b4d5ea816ef516dcea70985c610683579a9e2ac540c2d59b9a3ed27eedaff830a43a1c43c1683556a457c92ac66e09109ee995ab173090e4042c4c4bb03
languageName: node
linkType: hard
"@panva/hkdf@npm:^1.0.2":
version: 1.0.4
resolution: "@panva/hkdf@npm:1.0.4"
@ -23936,13 +23876,6 @@ __metadata:
languageName: node
linkType: hard
"jose@npm:4.11.1":
version: 4.11.1
resolution: "jose@npm:4.11.1"
checksum: cd15cba258d0fd20f6168631ce2e94fda8442df80e43c1033c523915cecdf390a1cc8efe0eab0c2d65935ca973d791c668aea80724d2aa9c2879d4e70f3081d7
languageName: node
linkType: hard
"jose@npm:4.12.0":
version: 4.12.0
resolution: "jose@npm:4.12.0"
@ -27522,13 +27455,6 @@ __metadata:
languageName: node
linkType: hard
"oauth4webapi@npm:2.0.5":
version: 2.0.5
resolution: "oauth4webapi@npm:2.0.5"
checksum: 32d0cb7b1cca42d51dfb88075ca2d69fe33172a807e8ea50e317d17cab3bc80588ab8ebcb7eb4600c371a70af4674595b4b341daf6f3a655f1efa1ab715bb6c9
languageName: node
linkType: hard
"oauth@npm:^0.9.15":
version: 0.9.15
resolution: "oauth@npm:0.9.15"
@ -29201,17 +29127,6 @@ __metadata:
languageName: node
linkType: hard
"preact-render-to-string@npm:5.2.3":
version: 5.2.3
resolution: "preact-render-to-string@npm:5.2.3"
dependencies:
pretty-format: ^3.8.0
peerDependencies:
preact: ">=10"
checksum: 6e46288d8956adde35b9fe3a21aecd9dea29751b40f0f155dea62f3896f27cb8614d457b32f48d33909d2da81135afcca6c55077520feacd7d15164d1371fb44
languageName: node
linkType: hard
"preact-render-to-string@npm:^5.1.19":
version: 5.2.6
resolution: "preact-render-to-string@npm:5.2.6"
@ -29223,7 +29138,7 @@ __metadata:
languageName: node
linkType: hard
"preact@npm:10.11.3, preact@npm:^10.6.3":
"preact@npm:^10.6.3":
version: 10.11.3
resolution: "preact@npm:10.11.3"
checksum: 9387115aa0581e8226309e6456e9856f17dfc0e3d3e63f774de80f3d462a882ba7c60914c05942cb51d51e23e120dcfe904b8d392d46f29ad15802941fe7a367
@ -33974,6 +33889,15 @@ __metadata:
languageName: node
linkType: hard
"tailwindcss-animate@npm:^1.0.6":
version: 1.0.6
resolution: "tailwindcss-animate@npm:1.0.6"
peerDependencies:
tailwindcss: "*"
checksum: 01471cb5e64d0936b3dc90bdb1d4b379e8b73b3d285553337d8576e5d458e868ddff99c6db0ae454c1be69ce8f88dc0eed44ea62e4d7bca10d7d2479fc4c8ee0
languageName: node
linkType: hard
"tailwindcss-radix@npm:^2.6.0":
version: 2.6.0
resolution: "tailwindcss-radix@npm:2.6.0"