Compare commits

...
63 Commits
Author SHA1 Message Date
Alex van Andel e7f557f7f9 v2.2.2
release.yml / release (push) Canceled after 0s
2022-11-18 14:52:34 +00:00
Alex van Andel e2921c3126 Techdebt/improve sentry config (#5585)
* Fix console errors during build

* Create source maps for production
2022-11-18 14:23:14 +00:00
Jeroen Reumkens d8102615b1 Upgrade Sentry to latest to add excludeServerRoutes in order to exclude OG Edge endpoint since Sentry won't work on the Edge yet, but WILL crash the api route. (#5574) 2022-11-18 09:49:19 +00:00
1010e2894a Bringing back sendgrid app to review (#5501)
* Sendgrid app and code simplification

* Applying app-store-cli + impl

* Fixing types

* Adding features to readme

* Fixing unit tests

* A few last tweaks regarding UX and env vars

* Applying feedback

* Using calcom icons

* Renaming and applying feedback

* Testing user/type page fix

* Standarizing Sendgrid client usage

* Removing types

* Reverting CloseCom changes

* Stop relying on sendgrid client pkg

* Fixing button and more reverting closecom changes

* Revert "Stop relying on sendgrid client pkg"

This reverts commit dd61851572a17a1e4051b133683af85c934bc2d0.

* Revert "Removing types"

This reverts commit 1ec5ed8de2f3139bbe84f867f229bc5759256806.

* Is this it?

* Standardizing apis

* Fixing path

* Fixing throwing errors the standard way

* Stop relying on getInstalledAppPath

* Removing seemingly troubling code

* Returning error and avoiding any outer reference

* Revert "Returning error and avoiding any outer reference"

This reverts commit 7d32e30154423c95f54ebae81a76ab16a1c7bc94.

* Revert "Removing seemingly troubling code"

This reverts commit eaae772abcd04c8f046e4960116f42c5aaf87adf.

* Revert "Stop relying on getInstalledAppPath"

This reverts commit bcc70fc337bbe7fb5e74609abaeee7cd3ede90a3.

* Revert "Fixing throwing errors the standard way"

This reverts commit bb1bb410fac6f8c6ad14c3163a8433d125f7a885.

* Revert "Fixing path"

This reverts commit a7bd83c4fb7597594d0470cb530378c826b45481.

* Revert "Standardizing apis"

This reverts commit 0258a182298af3ebad321854ef4f34a65f4c700a.

* Revert "Is this it?"

This reverts commit 70b3f7b98e3003dfa225dc539e02a1e17abdd840.

* Converting APIs to legacy style

* Missing reverted CloseCom test mock

* Needed for the renaming

* Reverting Closecom and yarn unneeded changes

* Ununsed type

* Testing rearranging exports

* Update apps/web/components/apps/OmniInstallAppButton.tsx

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

* Standardizing APIs

* Fixing wrong toast message on app page

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2022-11-17 21:38:34 +00:00
Udit TakkarandAlex van Andel c728174c14 fix: return when success status 204 (#5562)
* fix: return when success status 204

* fix: handle login in handleZoomResponse

* Add response.status check after the response.ok check

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-11-17 21:27:22 +00:00
Udit TakkarandPeer Richelsen 3889b49fa1 fix: use zod for validation (#5498)
* fix: use zod for validation

* fix: use slug instead of url

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-17 18:56:47 +00:00
sean-brydon 461389fb0b increase limits (#5554) 2022-11-17 09:17:07 +00:00
Jeroen Reumkens a8186e3039 Fixed wrong calculation of duration type on event type page. (#5557) 2022-11-17 13:38:03 +05:30
zomars 5e2db12396 Update release.yml 2022-11-16 15:17:35 -07:00
zomars 6bc9d2fd94 Update release.yml 2022-11-16 15:06:20 -07:00
zomars 37f9bbf3fa v2.2.1
release.yml / release (push) Canceled after 0s
2022-11-16 15:01:57 -07:00
zomars fb5b3bbcc6 Fixes team members avatar shrinkage 2022-11-16 15:01:25 -07:00
zomars d43ee154d0 Update release.yml 2022-11-16 14:38:15 -07:00
zomars 6415d42a1d v2.2.0
release.yml / release (push) Canceled after 0s
2022-11-16 14:22:08 -07:00
Omar Lópezandalannnc ee620c7fff Adds team upgrade banner (#5541)
* Adds team upgrade banner

* Update TeamListItem.tsx

* Update payments.ts

* Copy updates and mobile style fixes

* Update packages/trpc/server/routers/viewer/teams.tsx

Co-authored-by: alannnc <alannnc@gmail.com>

Co-authored-by: alannnc <alannnc@gmail.com>
2022-11-16 21:07:20 +00:00
Jeroen Reumkens 112efbbda2 CAL-300 Ensure we show the default calendar name based in the advanced tab for event types. (#5551) 2022-11-16 20:03:19 +00:00
fac0b0fa32 Improving cancellation flow (#5447)
* Booking succes query refactor

The query is now using the uid as its main identifier for the success page

* Minor changes to the succes.tsx and tests

* Convert eventtype dates to string, and only select eventtype slug from db to have a smaller query (we don't need more data, and this way we don't need to convert the dates in here to smaller strings either.)

* In the payment component get the bookingUid from props instead of the query

* design fixes

* fix UI for recurring bookings

* fix success page for recurring events

* fix cancelling recurring events

* fix design of successful cancellation page

* fix email redirect + design fixes

* remove old cancel page

* fix v2 design for text area

* Changed the recurringMutation to use the uid for the success booking page

* redirect form old to new cancel page

* fix success page for recurring events

* rename to allRemainingbookings

* fix old cancel redirect

* fix recurring cancel link in email

* fix redirect when cancelling one recurring booking

* remove reschedule for recurring events in email

* create queryschema for cancel success page

* scroll down to bottom if cancel

* add cancellation reason

* fix tests

* fix old /cancel component

* code clean up

* Uses URL query as cancellation state

Also fixes string to boolean inference

Co-authored-by: Mischa Rouleaux <mischa-rouleaux@live.nl>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2022-11-16 19:48:17 +00:00
Jeroen Reumkens 12d47ab949 Update UI components to be able to use them in Console (#5514)
* First steps in updating console ui components.

* Replaced remaining v1 components.

* Removed todo.

* Removed createelement from form component since we were not using it at all (we wrapped all our inputs in divs), and any input that was not wrapped, was actually broken...

* Undo update submodule.

* Reverted website submodule update.
2022-11-16 13:41:40 +00:00
4fef7ba5e2 Feat/top banner (#5443)
* wip

* wip

* chore: remove unused import

* feat: top banner component added

* feat: top banner stories added

* fix: icon bug

* Update packages/ui/components/topbanner/TopBanner.tsx

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

* Apply suggestions from code review

* Export fixes

* Feedback

* Type fixes

* Fixes

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-15 12:33:59 -07:00
6af0428a18 Booking success query refactor (#5298)
* Booking succes query refactor

The query is now using the uid as its main identifier for the success page

* Minor changes to the succes.tsx and tests

* Convert eventtype dates to string, and only select eventtype slug from db to have a smaller query (we don't need more data, and this way we don't need to convert the dates in here to smaller strings either.)

* In the payment component get the bookingUid from props instead of the query

* Changed the recurringMutation to use the uid for the success booking page

Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-15 12:00:02 -07:00
Jeroen Reumkensandkodiakhq[bot] d66f3d1dc9 Event row style improvements (#5533)
* Made full row of event type clickable, and fixed event type loader dimensions.

* Fixed skeletons for teams and workflows.

* Fixed skeleton for workflow detail page.

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-15 15:30:24 +00:00
Udit Takkar a5249b6cb5 fix: use tooltip and max-w (#5531) 2022-11-15 14:24:23 +00:00
Udit Takkar 6771c11360 fix: remove type password from fields (#5530) 2022-11-15 14:08:25 +00:00
002375ac72 fix langauge in booking submitted email for attendee (#5526)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-11-15 13:55:02 +00:00
Alex van Andel 0402e46328 Reformat /booking attendees, add guests to success page, fix i18n (#5532) 2022-11-15 13:39:06 +00:00
Jeroen ReumkensandPeer Richelsen 02e3738a17 Improved event description truncation (actually removed it mostly) (#5374)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-15 08:36:20 +00:00
Alex van Andel f822177788 Use Intl.DateTimeFormat logic to determine locale. (#5519) 2022-11-14 19:46:58 +00:00
Alex van Andel 1edc9c50d5 Removed an invalidate onDelete + updated test (#5521) 2022-11-14 19:34:47 +00:00
Syed Ali Shahbaz 7beb17c007 stops content pushing on windows browsers due to overflow-y (#5513) 2022-11-14 11:07:44 +00:00
Hariom Balharaandkodiakhq[bot] ad7af451ff Fix text getting changed after browsing through Reporting tab (#5510)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-14 10:53:35 +00:00
f513ce8687 Removed unused ui components and updated skeleton components (#5412)
* Removed unused ui components and updated skeleton components throughout the app to use new version.

* Deleted unused app component and changed destructive button variants to secondary as per design.

Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-14 10:31:36 +00:00
d83883e7e4 Minimum booking notice will allow hours and days (#5217)
* minimum booking notice allows hours and days

* Fixed error with Prisma not updating properly and input changing value to minutes autmatically

* fix type error

* Update apps/web/components/v2/eventtype/EventLimitsTab.tsx

Co-authored-by: Peer Richelsen <peer@cal.com>

* moved minimumBookingNotice to clientside, removed migration from prisma, changed DurationField to regular InputField

* Update packages/lib/convertMinimumBookingNoticeToMinutes.ts

Co-authored-by: alannnc <alannnc@gmail.com>

* added MINUTES_IN_HOUR, MINUTES_IN_DAY, and HOURS_IN_DAY as exportable consts

* Fix some comparison, mobile styles and const usages

* minimumBookingNotice values will never be floats

* fixed issue with minbookingnotice not updating properly in prisma

* Removed console.logs, converted minimumBookingNoticeType to ref, removed unreliable accessing of array

* remove yarn lockfile

* Revert "remove yarn lockfile"

This reverts commit fefc24c80269f5445c8eb7a640e2816d7d8b0407.

* Revert "Revert "remove yarn lockfile""

This reverts commit c8cc4d3c0eddb2c91d077d2277ad5ea10315ec95.

* Replaced currentDurationType export with state passed down from parent

* Improvements for duration notice type logic

* Undo adding submodule updates

* Update apps/web/components/eventtype/EventLimitsTab.tsx

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* Update packages/lib/findDurationType.ts

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

* Update apps/web/components/eventtype/EventLimitsTab.tsx

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

* Update packages/lib/convertToNewDurationType.ts

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

* Update apps/web/components/eventtype/EventLimitsTab.tsx

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

* Update apps/web/test/lib/getSchedule.test.ts

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

* remove passing in as object

* Update apps/web/test/lib/getSchedule.test.ts

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

* Update packages/lib/convertToNewDurationType.ts

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

* Delete convertMinimumBookingNoticeToMinutes.ts

* Syntax fix

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Omar López <zomars@me.com>
2022-11-14 09:12:28 +00:00
9e001c53e8 add tests (#5487)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-14 08:50:09 +00:00
Hariom BalharaandPeer Richelsen 3937b0c4c6 Add support to open a redirect URL in new tab (#5480)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-13 15:51:31 +00:00
Udit TakkarandPeer Richelsen cbe358bb6b fix: booking rejection message (#5497)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-12 21:21:01 +00:00
Leo Giovanetti 2b91102f3f Recurring emails fixed (#5499) 2022-11-12 16:46:27 +00:00
Udit Takkar 4e7804c46d fix: change max width for mobile (#5495) 2022-11-12 13:43:04 +00:00
Udit Takkar d7226fc3d4 fix: disable button and handle submit when loading (#5496) 2022-11-12 13:31:11 +00:00
Alex van Andel 0cb278d109 Update engines to >=16.x in apps/web and monorepo (#5493) 2022-11-11 21:35:38 +00:00
alannncandOmar López a1430101d1 fix/team-billing-update-publish-form (#5476)
* Display team slug on form when saved on metadata

* remove button disabled prop

* update mutation to latest trcp style

* Update packages/trpc/server/routers/viewer/teams.tsx

Co-authored-by: Omar López <zomars@me.com>
2022-11-11 20:38:21 +00:00
9092addd9c fix: use .watch() to disable update btn (#5482)
* fix: use .watch() to disable update btn

* Apply suggestions from code review

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-11-11 20:27:09 +00:00
Masumi Kawasakiandalannnc 7f14e00119 Correction of unclear Japanese translation (#5455)
Co-authored-by: alannnc <alannnc@gmail.com>
2022-11-11 20:05:46 +00:00
Omar Lópezandkodiakhq[bot] abc5ff2d97 Deprecated API endpoints cleanup (#5478)
* Deprecated API endpoints cleanup

* Linting

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-11 19:54:44 +00:00
Udit TakkarandPeer Richelsen fcde1f7a9d fix: ui bugs in apps (#5465)
* fix: ui bugs in apps

* fix: recurring text

* fix: logo size and font

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-11 19:35:10 +00:00
Udit TakkarandOmar López 30eea089ba fix: use outline none (#5486)
* fix: use outline none

* fix: dropdown trigger ui bugs

* Update packages/ui/Dropdown.tsx

Co-authored-by: Omar López <zomars@me.com>
2022-11-11 12:21:56 -07:00
zomars 3c8cb3a9ea Upgaded Snaplet CLI 2022-11-11 10:32:35 -07:00
54f4e665a3 Add alphanumeric sender ID to SMS workflow actions (#5471)
* add sender id

* add sender to twilio from

* added missing sender

* add migration

* fix design of add action dialog

* add cal as sender when creating new workflow

* fix type errors

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-11-11 15:01:17 +00:00
Alex van Andel ef3e7fae20 v2.1.5
release.yml / release (push) Canceled after 0s
2022-11-11 11:46:17 +00:00
Hariom Balharaandkodiakhq[bot] bb7815464c Feature/Routing-Forms/Reporting (#5375)
* Add very basic reporting logic

* Support more logical operators

* Add tests

* Fix types

* Add playwright test for reporting

* Fix table UI

* Remove only

* Better name variable

* Increase max_old_space to allow tests to run without reaciing limit

* Update test name

* Reuse getServerSideProps

* Variable renaming

* PR Suggestions

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-11 09:57:44 +00:00
Alex van AndelandOmar López acb6f292ec Bugfix/revert e5ba8616 (#5479)
* Revert e5ba8616

* Revert "Reverted Sentry for debug purposes (#5425)"

This reverts commit 4466c4ad4b.

Co-authored-by: Omar López <zomars@me.com>
2022-11-11 09:47:11 +00:00
0b2250caf9 fix: routing form edit button (#5466)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2022-11-11 15:01:13 +05:30
Julius Marmingeandzomars db7c3fb52f feat: trpc v10 migration (#5332)
* migrate router

* createTRPCReact

* frontend 1

* random format change

* frontend 2

* withQuery

* form router

* TS-ERROR: proxy on utils-client

* inferance

* ssg

* reuse

* trpc rc4

* Apply suggestions from code review

* skip test

* move skip one level up

* whops

* rc 6 with new setData func

Co-authored-by: zomars <zomars@me.com>
2022-11-10 23:40:01 +00:00
6d67808627 Team billing (#5453)
* WIP teams billing page

* WIP

* Create settings page

* Remove unused imports

* Create stripe customer on team creation

* Add Stripe ids to team record

* Add Stripe price ids for team to .env

* Create & delete Stripe customers

* Add string

* Merge branch 'main' into v2/teams-billing

* Create checkout session when creating team

* Create webhook to update team with Stripe ids

* Add Stripe migration files

* Move deleting team from Stripe under ee

* Some cleanup

* Merge branch 'v2/teams-billing' of https://github.com/calcom/cal.com into v2/teams-billing

* Small clean up

* Link to team's portal page

* Fix types

* Fix type errors

* Fix type errors

* Fix type error

* Delete old files & type fixes

* Address feedback

* Fix type errors

* Removes team creation modal

* WIP

* Removed billing frequency from team creation

* Add Stripe check for delete team customer

* Merge branch 'v2/teams-billing' of https://github.com/calcom/cal.com into v2/teams-billing

* Add high level form to create new team

* WIP

* Add new team to form

* Validate for invited members

* Add translations

* WIP

* Add validation for team name

* Add validation to team slug

* Clean up

* Fix type error

* Fix type errors

* WIP

* Abstract invite members function

* Add subscription status column

* Hide pending teams from settings

* Send email on paid subscription

* WIP

* Sync packages

* Add team subscription cols to schema

* WIP

* Matches locks vite version to <3

* Removed subscriptionStatus

* WIP

* Fix warning

* Query optimizations

* WIP

* Cleanup

* Wip

* WIP

* Runtime error fixes

* Cancellation fixes

* Delete team fixes

* Cleanup

* Type fixes

* Allows to check memebership in getTeamWithMembers

* Adds team creation tests

* Cleanup

* Cleanup

* Restored change

* Updated copy

* Moved component

* Cleanup

* Fix team members view

* Cleanup

* Adds failsafe for skipping publishing on update

* Cleanup

* Feedback

* More feedback

* Cleanup

* Cleanup

* Feedback

* Feedback

* Feedback

* Adds edge-case for slug conflicts

* Feedback

* e2e fixes

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-10 13:23:56 -07:00
Nafees Nazikandkodiakhq[bot] 557b57a261 fix: floating pop up button embed modal size (#5415)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-10 13:09:19 +00:00
31875f7535 Test Preview for routing forms (#5436)
* add first version of testing routing forms

* small design changes

* use new shared component

* readd deleted code for showing result route

* add form validation for required fields

* design fixes

* add scroll

* fix design of select

* use old dialog with correct overflow behaviour

* code clean up

* remove unused import

* fix mobile view

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-10 12:58:07 +00:00
29c4efe4a8 Delete slack app (#5462)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-10 11:54:15 +00:00
Udit Takkarandkodiakhq[bot] 7bda15aaa3 fix: ui of webhook segment (#5452)
* fix: ui of webhook segment

* feat: use toggle group component

* fix: default bug

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-10 11:38:40 +00:00
4176086ce0 Removed v1 component split for event types component (#5399)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-10 11:23:55 +00:00
Hariom Balharaandkodiakhq[bot] 04e62a10e6 In selfhosted, calvideo isnt default and thus an event can be created without location. Handle that (#5457)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-10 11:07:16 +00:00
Carina WollendorferandCarinaWolli 0ff38dece2 FIx button text (#5456)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-11-10 09:07:26 +00:00
Hariom Balhara 8416b76bf6 Fix flakiness (#5454) 2022-11-10 07:45:10 +00:00
zomars c3a427346d Matches locks vite version to <3
# Conflicts:
#	yarn.lock
2022-11-09 16:42:49 -07:00
Udit Takkar a73b3fcaed fix: use Intl to format month name (#5449) 2022-11-09 19:14:01 +00:00
403 changed files with 8481 additions and 10492 deletions
+6 -6
View File
@@ -96,11 +96,10 @@ NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE=
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE=
STRIPE_TEAM_MONTHLY_PRICE_ID=
STRIPE_WEBHOOK_SECRET=
STRIPE_PRO_PLAN_PRODUCT_ID=
STRIPE_PREMIUM_PLAN_PRODUCT_ID=
STRIPE_FREE_PLAN_PRODUCT_ID=
STRIPE_PRIVATE_KEY=
STRIPE_CLIENT_ID=
# Use for internal Public API Keys and optional
API_KEY_PREFIX=cal_
@@ -138,5 +137,6 @@ CLOSECOM_API_KEY=
# Sendgrid internal sync service
SENDGRID_SYNC_API_KEY=
# Sendgrid internal email sender
SENDGRID_API_KEY=
# Sentry
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_IGNORE_API_RESOLUTION_ERROR=
+6 -1
View File
@@ -1,4 +1,5 @@
on:
workflow_dispatch:
push:
# Pattern matched against refs/tags
tags:
@@ -8,4 +9,8 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- run: curl -X POST -d {} ${{ secrets.VERCEL_VERSIONED_DEPLOY_HOOK }}
- uses: actions/checkout@v3
with:
fetch-depth: 0
token: ${{ secrets.GH_ACCESS_TOKEN }}
- run: git push origin +main:production
+3 -2
View File
@@ -1,7 +1,7 @@
name: Unit tests
on:
push:
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
branches: [ feat/routing-forms/reporting ] # TODO: Remove this after merged in main
pull_request_target: # So we can test on forks
branches:
- main
@@ -21,7 +21,8 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
fetch-depth: 2
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
- run: echo 'NODE_OPTIONS="--max_old_space_size=6144"' >> $GITHUB_ENV
- name: Use Node 16.x
uses: actions/setup-node@v3
with:
+1 -1
View File
@@ -1,4 +1,4 @@
{
"projectId": "cl4u26bwz7962859ply7ibuo43t",
"databaseUrl": "postgresql://postgres@localhost:5450/calendso"
"targetDatabaseUrl": "postgresql://postgres@localhost:5450/calendso"
}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"version": "0.3.0"
"version": "0.22.3"
}
-50
View File
@@ -354,56 +354,6 @@ following
5. Use **Application (client) ID** as the **MS_GRAPH_CLIENT_ID** attribute value in .env
6. Click **Certificates & secrets** create a new client secret and use the value as the **MS_GRAPH_CLIENT_SECRET** attribute
### Obtaining Slack Client ID and Secret and Signing Secret
To test this you will need to create a Slack app for yourself on [their apps website](https://api.slack.com/apps).
Copy and paste the app manifest below into the setting on your slack app. Be sure to replace `YOUR_DOMAIN` with your own domain or your proxy host if you're testing locally.
<details>
<summary>App Manifest</summary>
```yaml
display_information:
name: Cal.com Slack
features:
bot_user:
display_name: Cal.com Slack
always_online: false
slash_commands:
- command: /create-event
url: https://YOUR_DOMAIN/api/integrations/slackmessaging/commandHandler
description: Create an event within Cal!
should_escape: false
- command: /today
url: https://YOUR_DOMAIN/api/integrations/slackmessaging/commandHandler
description: View all your bookings for today
should_escape: false
oauth_config:
redirect_urls:
- https://YOUR_DOMAIN/api/integrations/slackmessaging/callback
scopes:
bot:
- chat:write
- commands
- chat:write.public
settings:
interactivity:
is_enabled: true
request_url: https://YOUR_DOMAIN/api/integrations/slackmessaging/interactiveHandler
message_menu_options_url: https://YOUR_DOMAIN/api/integrations/slackmessaging/interactiveHandler
org_deploy_enabled: false
socket_mode_enabled: false
token_rotation_enabled: false
```
</details>
Add the integration as normal - slack app - add. Follow the oauth flow to add it to a server.
Next make sure you have your app running `yarn dx`. Then in the slack chat type one of these commands: `/create-event` or `/today`
> NOTE: Next you will need to setup a proxy server like [ngrok](https://ngrok.com/) to allow your local host machine to be hosted on a public https server.
### Obtaining Zoom Client ID and Secret
+4 -3
View File
@@ -3,11 +3,12 @@ import { classNames } from "@calcom/lib";
interface ExampleProps {
children: React.ReactNode;
title: string;
isFullWidth?: boolean;
}
export const Example = ({ children, title }: ExampleProps) => {
export const Example = ({ children, title, isFullWidth = false }: ExampleProps) => {
return (
<div className="examples-item">
<span className="examples-item-title">{title}</span>
<div className={classNames("examples-item", isFullWidth && "w-full")}>
<span className={classNames("examples-item-title", isFullWidth && "mb-0 mt-2")}>{title}</span>
<div className="examples-item-content">{children}</div>
</div>
);
+1 -1
View File
@@ -49,6 +49,6 @@
"storybook-react-i18next": "^1.1.2",
"tailwindcss": "^3.2.1",
"typescript": "^4.7.4",
"vite": "^3.1.0"
"vite": "^2.9.15"
}
}
+3 -1
View File
@@ -67,4 +67,6 @@ tsconfig.tsbuildinfo
public/embed
# Copied app-store images
public/app-store
public/app-store
# Sentry
.sentryclirc
-119
View File
@@ -598,60 +598,6 @@ paths:
summary: Gets and stores the OAuth token
tags:
- Integrations
/api/user/profile:
patch:
description: Updates a user's profile.
summary: Updates a user's profile
tags:
- User
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
description:
type: string
avatar:
type: string
timeZone:
type: string
weekStart:
type: string
hideBranding:
type: string
theme:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
message:
type: string
"401":
description: Unauthorized
content:
application/json:
schema:
type: object
properties:
message:
type: string
"500":
description: Internal Server Error
content:
application/json:
schema:
type: object
properties:
message:
type: string
/api/me:
get:
description: Gets current user's profile.
@@ -686,71 +632,6 @@ paths:
properties:
message:
type: string
/api/user/membership:
get:
description: Get a list of the teams the user has joined.
summary: Get the teams a user is joined to
tags:
- User
requestBody:
content:
application/json:
schema:
type: object
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
message:
type: string
"401":
description: Unauthorized
content:
application/json:
schema:
type: object
properties:
message:
type: string
"500":
description: Internal Server Error
content:
application/json:
schema:
type: object
properties:
message:
type: string
patch:
description: Accept team invitation
summary: Accept team invitation.
tags:
- User
requestBody:
content:
application/json:
schema:
type: object
properties:
teamId:
type: string
delete:
description: Leave team or decline membership invite of current user
summary: Leave team or decline team invite.
tags:
- User
requestBody:
content:
application/json:
schema:
type: object
properties:
teamId:
type: string
"/api/{team}":
delete:
description: Deletes a team
@@ -36,7 +36,7 @@ const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.integrations", { variant: "calendar", onlyInstalled: true }]);
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
return (
<QueryCell
-306
View File
@@ -1,306 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useState } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton } from "@calcom/app-store/components";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { App as AppType } from "@calcom/types/App";
import { Button, SkeletonButton } from "@calcom/ui";
import { Icon } from "@calcom/ui/Icon";
import Shell from "@calcom/ui/Shell";
import { Badge } from "@calcom/ui/components/badge";
import showToast from "@calcom/ui/v2/core/notifications";
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
const Component = ({
name,
type,
logo,
body,
categories,
author,
price = 0,
commission,
isGlobal = false,
feeType,
docs,
website,
email,
tos,
privacy,
isProOnly,
}: Parameters<typeof App>[0]) => {
const { t } = useLocale();
const router = useRouter();
const { data: user } = trpc.useQuery(["viewer.me"]);
const utils = trpc.useContext();
const handleOpenChange = () => {
utils.invalidateQueries(["viewer.integrations"]);
router.replace("/apps/installed");
};
const mutation = useAddAppMutation(null, {
onSuccess: () => {
showToast("App successfully installed", "success");
},
onError: (error) => {
if (error instanceof Error) showToast(error.message || "App could not be installed", "error");
},
});
const priceInDollar = Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price);
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
const appCredentials = trpc.useQuery(["viewer.appCredentialsByType", { appType: type }], {
onSuccess(data) {
setExistingCredentials(data);
},
});
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
return (
<div className="-mx-4 md:-mx-8">
<div className="px-8">
<Link href="/apps">
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800">
<Icon.FiChevronLeft className="h-5 w-5" /> {t("browse_apps")}
</a>
</Link>
<div className="items-center justify-between py-4 sm:flex sm:py-8">
<div className="flex">
<img className="h-16 w-16 rounded-sm" src={logo} alt={name} />
<header className="px-4 py-2">
<div className="flex items-center">
<h1 className="font-cal text-xl text-gray-900">{name}</h1>
</div>
<h2 className="text-sm text-gray-500">
<span className="capitalize">{categories[0]}</span> • {t("published_by", { author })}
</h2>
</header>
</div>
<div className="mt-4 sm:mt-0 sm:text-right">
{!appCredentials.isLoading ? (
isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3">
<Button StartIcon={Icon.FiCheck} color="secondary" disabled>
{existingCredentials.length > 0
? t("active_install", { count: existingCredentials.length })
: t("globally_install")}
</Button>
{!isGlobal && (
<InstallAppButton
type={type}
isProOnly={isProOnly}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
onClick: () => {
mutation.mutate({ type });
},
loading: mutation.isLoading,
};
}
return (
<Button StartIcon={Icon.FiPlus} {...props} data-testid="install-app-button">
{t("add_another")}
</Button>
);
}}
/>
)}
</div>
) : existingCredentials.length > 0 ? (
<DisconnectIntegration
id={existingCredentials[0]}
render={(btnProps) => (
<Button {...btnProps} color="warn" data-testid={type + "-integration-disconnect-button"}>
{t("disconnect")}
</Button>
)}
onOpenChange={handleOpenChange}
/>
) : (
<InstallAppButton
type={type}
isProOnly={isProOnly}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
onClick: () => {
mutation.mutate({ type });
},
loading: mutation.isLoading,
};
}
return (
<Button data-testid="install-app-button" {...props}>
{t("install_app")}
</Button>
);
}}
/>
))
) : (
<SkeletonButton width="24" height="10" />
)}
{price !== 0 && (
<small className="block text-right">
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
{feeType === "monthly" && "/" + t("month")}
</small>
)}
</div>
</div>
</div>
<div className="justify-between px-8 py-10 md:flex">
<div className="prose-sm prose mb-6">{body}</div>
<div className="md:max-w-80 flex-1 md:ml-8">
<h4 className="font-medium text-gray-900 ">{t("categories")}</h4>
<div className="space-x-2">
{categories.map((category) => (
<Link href={"/apps/categories/" + category} key={category}>
<a>
<Badge variant="success">{category}</Badge>
</a>
</Link>
))}
</div>
<h4 className="mt-8 font-medium text-gray-900 ">{t("pricing")}</h4>
<small>
{price === 0 ? (
"Free"
) : (
<>
{Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price)}
{feeType === "monthly" && "/" + t("month")}
</>
)}
</small>
<h4 className="mt-8 mb-2 font-medium text-gray-900 ">{t("learn_more")}</h4>
<ul className="prose -ml-1 -mr-1 text-xs leading-5">
{docs && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={docs}>
<Icon.FiBookOpen className="mr-1 -mt-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
)}
{website && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={website}>
<Icon.FiExternalLink className="mr-1 -mt-px inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
)}
{email && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={"mailto:" + email}>
<Icon.FiMail className="mr-1 -mt-px inline h-4 w-4" />
{email}
</a>
</li>
)}
{tos && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={tos}>
<Icon.FiFile className="mr-1 -mt-px inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
)}
{privacy && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={privacy}>
<Icon.FiShield className="mr-1 -mt-px inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
)}
</ul>
<hr className="my-6" />
<small className="leading-1 block text-gray-500">
Every app published on the Cal.com App Store is open source and thoroughly tested via peer
reviews. Nevertheless, Cal.com, Inc. does not endorse or certify these apps unless they are
published by Cal.com. If you encounter inappropriate content or behaviour please report it.
</small>
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com">
<Icon.FiFlag className="inline h-3 w-3" /> Report App
</a>
</div>
</div>
</div>
);
};
export default function App(props: {
name: string;
type: AppType["type"];
isGlobal?: AppType["isGlobal"];
logo: string;
body: React.ReactNode;
categories: string[];
author: string;
pro?: boolean;
price?: number;
commission?: number;
feeType?: AppType["feeType"];
docs?: string;
website?: string;
email: string; // required
tos?: string;
privacy?: string;
licenseRequired: AppType["licenseRequired"];
isProOnly: AppType["isProOnly"];
}) {
return (
<Shell large isPublic>
{props.licenseRequired ? (
<LicenseRequired>
<Component {...props} />
</LicenseRequired>
) : (
<Component {...props} />
)}
</Shell>
);
}
@@ -23,7 +23,7 @@ const DestinationCalendarSelector = ({
destinationCalendar,
}: Props): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.connectedCalendars"]);
const query = trpc.viewer.connectedCalendars.useQuery();
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
// Extra styles to show prefixed text in react-select
+208 -206
View File
@@ -801,245 +801,247 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
{embed.title}
</h3>
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible
open={isEmbedCustomizationOpen}
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
<CollapsibleTrigger
type="button"
className="flex w-full items-center text-base font-medium text-neutral-900">
<div>
{embedType === "inline"
? "Inline Embed Customization"
: embedType === "floating-popup"
? "Floating Popup Customization"
: "Element Click Customization"}
</div>
<Icon.FiChevronRight
className={`${
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-neutral-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent className="text-sm">
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-sm">Embed Window Sizing</div>
<div className="justify-left flex items-center">
<TextField
name="width"
labelProps={{ className: "hidden" }}
required
value={previewState.inline.width}
onChange={(e) => {
setPreviewState((previewState) => {
const width = e.target.value || "100%";
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<InputLeading>W</InputLeading>}
/>
<span className="p-2">×</span>
<TextField
labelProps={{ className: "hidden" }}
name="height"
value={previewState.inline.height}
required
onChange={(e) => {
const height = e.target.value || "100%";
setPreviewState((previewState) => {
return {
...previewState,
inline: {
...previewState.inline,
height,
},
};
});
}}
addOnLeading={<InputLeading>H</InputLeading>}
/>
<div className="max-h-97 flex flex-col overflow-y-auto">
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible
open={isEmbedCustomizationOpen}
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
<CollapsibleTrigger
type="button"
className="flex w-full items-center text-base font-medium text-neutral-900">
<div>
{embedType === "inline"
? "Inline Embed Customization"
: embedType === "floating-popup"
? "Floating Popup Customization"
: "Element Click Customization"}
</div>
</div>
<div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "" : "hidden"
)}>
<div className="mb-2 text-sm">Button Text</div>
{/* Default Values should come from preview iframe */}
<TextField
name="buttonText"
labelProps={{ className: "hidden" }}
onChange={(e) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonText: e.target.value,
},
};
});
}}
defaultValue="Book my Cal"
required
<Icon.FiChevronRight
className={`${
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-neutral-500`}
/>
</div>
<div
className={classNames(
"mt-4 flex items-center justify-start",
embedType === "floating-popup" ? "space-x-2" : "hidden"
)}>
<Switch
defaultChecked={true}
onCheckedChange={(checked) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
hideButtonIcon: !checked,
},
};
});
}}
/>
<div className="text-sm">Display Calendar Icon Button</div>
</div>
<div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "" : "hidden"
)}>
<div className="mb-2">Position of Button</div>
<Select
onChange={(position) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonPosition: position?.value,
},
};
});
}}
defaultValue={FloatingPopupPositionOptions[0]}
options={FloatingPopupPositionOptions}
/>
</div>
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div>Button Color</div>
<div className="w-full">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
</CollapsibleTrigger>
<CollapsibleContent className="text-sm">
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-sm">Embed Window Sizing</div>
<div className="justify-left flex items-center">
<TextField
name="width"
labelProps={{ className: "hidden" }}
required
value={previewState.inline.width}
onChange={(e) => {
setPreviewState((previewState) => {
const width = e.target.value || "100%";
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<InputLeading>W</InputLeading>}
/>
<span className="p-2">×</span>
<TextField
labelProps={{ className: "hidden" }}
name="height"
value={previewState.inline.height}
required
onChange={(e) => {
const height = e.target.value || "100%";
setPreviewState((previewState) => {
return {
...previewState,
inline: {
...previewState.inline,
height,
},
};
});
}}
addOnLeading={<InputLeading>H</InputLeading>}
/>
</div>
</div>
<div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "" : "hidden"
)}>
<div className="mb-2 text-sm">Button Text</div>
{/* Default Values should come from preview iframe */}
<TextField
name="buttonText"
labelProps={{ className: "hidden" }}
onChange={(e) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonColor: color,
buttonText: e.target.value,
},
};
});
}}
defaultValue="Book my Cal"
required
/>
</div>
</div>
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div>Text Color</div>
<div className="w-full">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
<div
className={classNames(
"mt-4 flex items-center justify-start",
embedType === "floating-popup" ? "space-x-2" : "hidden"
)}>
<Switch
defaultChecked={true}
onCheckedChange={(checked) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonTextColor: color,
hideButtonIcon: !checked,
},
};
});
}}
/>
<div className="text-sm">Display Calendar Icon Button</div>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>
<hr className="mt-4" />
<div className="mt-4 font-medium">
<Collapsible
open={isBookingCustomizationOpen}
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleTrigger className="flex w-full" type="button">
<div className="text-base font-medium text-neutral-900">Cal Booking Customization</div>
<Icon.FiChevronRight
className={`${
isBookingCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-neutral-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-6 text-sm">
<Label className="">
<div className="mb-2">Theme</div>
<div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "" : "hidden"
)}>
<div className="mb-2">Position of Button</div>
<Select
className="w-full"
defaultValue={ThemeOptions[0]}
components={{
Control: ThemeSelectControl,
}}
onChange={(option) => {
if (!option) {
return;
}
onChange={(position) => {
setPreviewState((previewState) => {
return {
...previewState,
theme: option.value,
floatingPopup: {
...previewState.floatingPopup,
buttonPosition: position?.value,
},
};
});
}}
options={ThemeOptions}
defaultValue={FloatingPopupPositionOptions[0]}
options={FloatingPopupPositionOptions}
/>
</Label>
{[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="pb-4">
<div className="mb-2 pt-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof typeof previewState["palette"]]: color,
});
}}
/>
</div>
</div>
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div>Button Color</div>
<div className="w-full">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonColor: color,
},
};
});
}}
/>
</div>
</div>
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div>Text Color</div>
<div className="w-full">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonTextColor: color,
},
};
});
}}
/>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>
<hr className="mt-4" />
<div className="mt-4 font-medium">
<Collapsible
open={isBookingCustomizationOpen}
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleTrigger className="flex w-full" type="button">
<div className="text-base font-medium text-neutral-900">Cal Booking Customization</div>
<Icon.FiChevronRight
className={`${
isBookingCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-neutral-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-6 text-sm">
<Label className="">
<div className="mb-2">Theme</div>
<Select
className="w-full"
defaultValue={ThemeOptions[0]}
components={{
Control: ThemeSelectControl,
}}
onChange={(option) => {
if (!option) {
return;
}
setPreviewState((previewState) => {
return {
...previewState,
theme: option.value,
};
});
}}
options={ThemeOptions}
/>
</Label>
))}
</div>
</CollapsibleContent>
</Collapsible>
{[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="pb-4">
<div className="mb-2 pt-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof typeof previewState["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
</div>
</CollapsibleContent>
</Collapsible>
</div>
</div>
</div>
<div className="flex w-2/3 flex-col p-8">
+1 -1
View File
@@ -4,7 +4,7 @@ import { useEffect } from "react";
import { trpc } from "@calcom/trpc/react";
export function useViewerI18n() {
return trpc.useQuery(["viewer.public.i18n"], {
return trpc.viewer.public.i18n.useQuery(undefined, {
staleTime: Infinity,
/**
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
@@ -42,7 +42,7 @@ const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.integrations", { variant: "calendar", onlyInstalled: true }]);
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
return (
<QueryCell
+11 -6
View File
@@ -43,7 +43,8 @@ const Component = ({
const router = useRouter();
const mutation = useAddAppMutation(null, {
onSuccess: () => {
onSuccess: (data) => {
if (data.setupPending) return;
showToast(t("app_successfully_installed"), "success");
},
onError: (error) => {
@@ -58,11 +59,14 @@ const Component = ({
}).format(price);
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
const appCredentials = trpc.useQuery(["viewer.appCredentialsByType", { appType: type }], {
onSuccess(data) {
setExistingCredentials(data);
},
});
const appCredentials = trpc.viewer.appCredentialsByType.useQuery(
{ appType: type },
{
onSuccess(data) {
setExistingCredentials(data);
},
}
);
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
@@ -143,6 +147,7 @@ const Component = ({
</div>
) : existingCredentials.length > 0 ? (
<DisconnectIntegration
buttonProps={{ color: "secondary" }}
label={t("disconnect")}
credentialId={existingCredentials[0]}
onSuccess={() => {
@@ -77,7 +77,7 @@ function CalendarSwitch(props: {
},
{
async onSettled() {
await utils.invalidateQueries(["viewer.integrations"]);
await utils.viewer.integrations.invalidate();
},
onError() {
showToast(`Something went wrong when toggling "${props.title}""`, "error");
@@ -107,7 +107,7 @@ function CalendarSwitch(props: {
function CalendarList(props: Props) {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.integrations", { variant: "calendar", onlyInstalled: false }]);
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
return (
<QueryCell
@@ -144,7 +144,9 @@ function CalendarList(props: Props) {
// todo: @hariom extract this into packages/apps-store as "GeneralAppSettings"
function ConnectedCalendarsList(props: Props) {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.connectedCalendars"], { suspense: true });
const query = trpc.viewer.connectedCalendars.useQuery(undefined, {
suspense: true,
});
const { fromOnboarding } = props;
return (
<QueryCell
@@ -235,19 +237,19 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
const utils = trpc.useContext();
const onChanged = () =>
Promise.allSettled([
utils.invalidateQueries(["viewer.integrations", { variant: "calendar", onlyInstalled: true }], {
exact: true,
}),
utils.invalidateQueries(["viewer.connectedCalendars"]),
utils.viewer.integrations.invalidate(
{ variant: "calendar", onlyInstalled: true },
{
exact: true,
}
),
utils.viewer.connectedCalendars.invalidate(),
]);
const query = trpc.useQuery(["viewer.connectedCalendars"]);
const installedCalendars = trpc.useQuery([
"viewer.integrations",
{ variant: "calendar", onlyInstalled: true },
]);
const mutation = trpc.useMutation("viewer.setDestinationCalendar", {
const query = trpc.viewer.connectedCalendars.useQuery();
const installedCalendars = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
const mutation = trpc.viewer.setDestinationCalendar.useMutation({
onSuccess: () => {
utils.invalidateQueries(["viewer.connectedCalendars"]);
utils.viewer.connectedCalendars.invalidate();
},
});
return (
@@ -23,7 +23,7 @@ const DestinationCalendarSelector = ({
destinationCalendar,
}: Props): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.connectedCalendars"]);
const query = trpc.viewer.connectedCalendars.useQuery();
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
// Extra styles to show prefixed text in react-select
@@ -4,6 +4,7 @@ import { ReactNode, useEffect, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui";
import { showToast } from "@calcom/ui/v2";
import { ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/core/List";
import classNames from "@lib/classNames";
@@ -27,6 +28,13 @@ function IntegrationListItem(props: {
const [highlight, setHighlight] = useState(hl === props.slug);
const title = props.name || props.title;
// The highlight is to show a newly installed app, coming from the app's
// redirection after installation, so we proceed to show the corresponding
// message
if (highlight) {
showToast(t("app_successfully_installed"), "success");
}
useEffect(() => {
const timer = setTimeout(() => setHighlight(false), 3000);
return () => {
@@ -18,10 +18,11 @@ export default function OmniInstallAppButton({ appId, className }: { appId: stri
const utils = trpc.useContext();
const mutation = useAddAppMutation(null, {
onSuccess: () => {
onSuccess: (data) => {
//TODO: viewer.appById might be replaced with viewer.apps so that a single query needs to be invalidated.
utils.invalidateQueries(["viewer.appById", { appId }]);
utils.invalidateQueries(["viewer.apps", { extendsFeature: "EventType" }]);
utils.viewer.appById.invalidate({ appId });
utils.viewer.apps.invalidate({ extendsFeature: "EventType" });
if (data.setupPending) return;
showToast(t("app_successfully_installed"), "success");
},
onError: (error) => {
@@ -7,7 +7,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { nameOfDay } from "@calcom/lib/weekday";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
import { SkeletonContainer, SkeletonText } from "@calcom/ui";
import { SkeletonContainer, SkeletonText } from "@calcom/ui/v2";
import { ToggleGroup } from "@calcom/ui/v2/core/form/ToggleGroup";
import classNames from "@lib/classNames";
@@ -152,13 +152,13 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
{isLoading && !slots.length && (
<>
<SkeletonContainer className="mb-2">
<SkeletonText width="full" height="20" />
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
<SkeletonContainer className="mb-2">
<SkeletonText width="full" height="20" />
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
<SkeletonContainer className="mb-2">
<SkeletonText width="full" height="20" />
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
</>
)}
+58 -72
View File
@@ -8,7 +8,7 @@ import classNames from "@calcom/lib/classNames";
import { formatTime } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { inferQueryInput, inferQueryOutput, trpc } from "@calcom/trpc/react";
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
import { Badge } from "@calcom/ui/components/badge";
@@ -24,13 +24,13 @@ import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import TableActions, { ActionType } from "@components/ui/TableActions";
type BookingListingStatus = inferQueryInput<"viewer.bookings">["status"];
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
type BookingItemProps = BookingItem & {
listingStatus: BookingListingStatus;
recurringInfo: inferQueryOutput<"viewer.bookings">["recurringInfo"][number] | undefined;
recurringInfo: RouterOutputs["viewer"]["bookings"]["get"]["recurringInfo"][number] | undefined;
};
function BookingListItem(booking: BookingItemProps) {
@@ -42,15 +42,19 @@ function BookingListItem(booking: BookingItemProps) {
const router = useRouter();
const [rejectionReason, setRejectionReason] = useState<string>("");
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
const mutation = trpc.useMutation(["viewer.bookings.confirm"], {
onSuccess: () => {
setRejectionDialogIsOpen(false);
showToast(t("booking_confirmation_success"), "success");
utils.invalidateQueries("viewer.bookings");
const mutation = trpc.viewer.bookings.confirm.useMutation({
onSuccess: (data) => {
if (data.status === BookingStatus.REJECTED) {
setRejectionDialogIsOpen(false);
showToast(t("booking_rejection_success"), "success");
} else {
showToast(t("booking_confirmation_success"), "success");
}
utils.viewer.bookings.invalidate();
},
onError: () => {
showToast(t("booking_confirmation_failed"), "error");
utils.invalidateQueries("viewer.bookings");
utils.viewer.bookings.invalidate();
},
});
@@ -108,7 +112,9 @@ function BookingListItem(booking: BookingItemProps) {
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
/* When cancelling we need to let the UI and the API know if the intention is to
cancel all remaining bookings or just that booking instance. */
href: `/cancel/${booking.uid}${isTabRecurring && isRecurring ? "?allRemainingBookings=true" : ""}`,
href: `/success?uid=${booking.uid}&cancel=true${
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}`,
icon: Icon.FiX,
},
{
@@ -158,11 +164,11 @@ function BookingListItem(booking: BookingItemProps) {
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
const setLocationMutation = trpc.useMutation("viewer.bookings.editLocation", {
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
onSuccess: () => {
showToast(t("location_updated"), "success");
setIsOpenLocationDialog(false);
utils.invalidateQueries("viewer.bookings");
utils.viewer.bookings.invalidate();
},
});
@@ -181,26 +187,14 @@ function BookingListItem(booking: BookingItemProps) {
.concat(booking.recurringInfo?.bookings[BookingStatus.PENDING])
.sort((date1: Date, date2: Date) => date1.getTime() - date2.getTime());
const location = booking.location || "";
const onClickTableData = () => {
router.push({
pathname: "/success",
query: {
date: booking.startTime,
// TODO: Booking when fetched should have id 0 already(for Dynamic Events).
type: booking.eventType.id || 0,
eventSlug: booking.eventType.slug,
username: user?.username || "",
name: booking.attendees[0] ? booking.attendees[0].name : undefined,
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
location: location,
eventName: booking.eventType.eventName || "",
bookingId: booking.id,
recur: booking.recurringEventId,
reschedule: isConfirmed,
uid: booking.uid,
allRemainingBookings: isTabRecurring,
listingStatus: booking.listingStatus,
status: booking.status,
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
},
});
};
@@ -467,8 +461,9 @@ const FirstAttendee = ({
user: UserProps;
currentEmail: string | null | undefined;
}) => {
const { t } = useLocale();
return user.email === currentEmail ? (
<div className="inline-block">You</div>
<div className="inline-block">{t("you")}</div>
) : (
<a
key={user.email}
@@ -480,19 +475,19 @@ const FirstAttendee = ({
);
};
const Attendee: React.FC<{ email: string; children: React.ReactNode }> = ({ email, children }) => {
type AttendeeProps = {
name?: string;
email: string;
};
const Attendee = ({ email, name }: AttendeeProps) => {
return (
<a className=" hover:text-blue-500" href={"mailto:" + email} onClick={(e) => e.stopPropagation()}>
{children}
<a className="hover:text-blue-500" href={"mailto:" + email} onClick={(e) => e.stopPropagation()}>
{name || email}
</a>
);
};
interface AttendeeProps {
name: string;
email: string;
}
const DisplayAttendees = ({
attendees,
user,
@@ -500,42 +495,33 @@ const DisplayAttendees = ({
}: {
attendees: AttendeeProps[];
user: UserProps | null;
currentEmail: string | null | undefined;
currentEmail?: string | null;
}) => {
if (attendees.length === 1) {
return (
<div className="text-sm text-gray-900">
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
<span>&nbsp;and&nbsp;</span>
<Attendee email={attendees[0].email}>{attendees[0].name}</Attendee>
</div>
);
} else if (attendees.length === 2) {
return (
<div className="text-sm text-gray-900">
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
<span>,&nbsp;</span>
<Attendee email={attendees[0].email}>{attendees[0].name}</Attendee>
<div className="inline-block text-sm text-gray-900">&nbsp;and&nbsp;</div>
<Attendee email={attendees[1].email}>{attendees[1].name}</Attendee>
</div>
);
} else {
return (
<div className="text-sm text-gray-900">
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
<span>,&nbsp;</span>
<Attendee email={attendees[0].email}>{attendees[0].name}</Attendee>
<span>&nbsp;&&nbsp;</span>
<Tooltip
content={attendees.slice(1).map((attendee, key) => (
<p key={key}>{attendee.name}</p>
))}>
<div className="inline-block">{attendees.length - 1} more</div>
</Tooltip>
</div>
);
}
const { t } = useLocale();
return (
<div className="text-sm text-gray-900">
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
{attendees.length > 1 ? <span>,&nbsp;</span> : <span>&nbsp;{t("and")}&nbsp;</span>}
<Attendee {...attendees[0]} />
{attendees.length > 1 && (
<>
<div className="inline-block text-sm text-gray-900">&nbsp;{t("and")}&nbsp;</div>
{attendees.length > 2 ? (
<Tooltip
content={attendees.slice(1).map((attendee) => (
<p key={attendee.email}>
<Attendee {...attendee} />
</p>
))}>
<div className="inline-block">{t("plus_more", { count: attendees.length - 1 })}</div>
</Tooltip>
) : (
<Attendee {...attendees[1]} />
)}
</>
)}
</div>
);
};
const Tag = ({ children, className = "" }: React.PropsWithChildren<{ className?: string }>) => {
+15 -20
View File
@@ -5,8 +5,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Button } from "@calcom/ui/Button";
import { Icon } from "@calcom/ui/Icon";
import { Button, TextArea } from "@calcom/ui/components";
type Props = {
booking: {
@@ -22,13 +22,14 @@ type Props = {
team?: string | null;
setIsCancellationMode: (value: boolean) => void;
theme: string | null;
allRemainingBookings: boolean;
};
export default function CancelBooking(props: Props) {
const [cancellationReason, setCancellationReason] = useState<string>("");
const { t } = useLocale();
const router = useRouter();
const { booking, profile, team } = props;
const { booking, profile, team, allRemainingBookings } = props;
const [loading, setLoading] = useState(false);
const telemetry = useTelemetry();
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
@@ -51,30 +52,23 @@ export default function CancelBooking(props: Props) {
{!error && (
<div className="mt-5 sm:mt-6">
<label className="text-bookingdark font-medium dark:text-white">{t("cancellation_reason")}</label>
<textarea
<TextArea
placeholder={t("cancellation_reason_placeholder")}
value={cancellationReason}
onChange={(e) => setCancellationReason(e.target.value)}
className="mt-2 mb-3 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white sm:mb-3 "
className="mt-2 mb-4 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white "
rows={3}
/>
<div className="flex flex-col-reverse rtl:space-x-reverse sm:flex-row">
{!props.recurringEvent && (
<div className="border-bookinglightest mt-5 flex w-full justify-center border-t pt-3 sm:mt-0 sm:justify-start sm:border-0 sm:pt-0">
<Button
color="secondary"
className="border-0 sm:border"
onClick={() => router.push(`/reschedule/${booking?.uid}`)}>
{t("reschedule_this")}
</Button>
</div>
)}
<div className="mb-2 flex w-full space-x-2 text-right sm:mb-0 ">
<Button color="secondary" onClick={() => props.setIsCancellationMode(false)}>
<div className="flex flex-col-reverse rtl:space-x-reverse ">
<div className="ml-auto flex w-full space-x-4 ">
<Button
className="ml-auto"
color="secondary"
onClick={() => props.setIsCancellationMode(false)}>
{t("nevermind")}
</Button>
<Button
className="flex grow justify-center"
className="flex justify-center"
data-testid="cancel"
onClick={async () => {
setLoading(true);
@@ -82,6 +76,7 @@ export default function CancelBooking(props: Props) {
const payload = {
id: booking?.id,
cancellationReason: cancellationReason,
allRemainingBookings,
};
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
@@ -98,7 +93,7 @@ export default function CancelBooking(props: Props) {
await router.push(
`/cancel/success?name=${props.profile.name}&title=${booking?.title}&eventPage=${
profile.slug
}&team=${team ? 1 : 0}&recurring=${!!props.recurringEvent}`
}&team=${team ? 1 : 0}&allRemainingBookings=${allRemainingBookings}`
);
} else {
setLoading(false);
@@ -110,7 +105,7 @@ export default function CancelBooking(props: Props) {
}
}}
loading={loading}>
{t("cancel_event")}
{props.allRemainingBookings ? t("cancel_all_remaining") : t("cancel_event")}
</Button>
</div>
</div>
@@ -79,19 +79,18 @@ const useSlots = ({
usernameList: string[];
timeZone?: string;
}) => {
const { data, isLoading, isPaused } = trpc.useQuery(
[
"viewer.public.slots.getSchedule",
{
eventTypeId,
eventTypeSlug,
usernameList,
startTime: startTime?.toISOString() || "",
endTime: endTime?.toISOString() || "",
timeZone,
},
],
{ enabled: !!startTime && !!endTime }
const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery(
{
eventTypeId,
eventTypeSlug,
usernameList,
startTime: startTime?.toISOString() || "",
endTime: endTime?.toISOString() || "",
timeZone,
},
{
enabled: !!startTime && !!endTime,
}
);
const [cachedSlots, setCachedSlots] = useState<NonNullable<typeof data>["slots"]>({});
@@ -122,7 +122,7 @@ const BookingPage = ({
const mutation = useMutation(createBooking, {
onSuccess: async (responseData) => {
const { id, paymentUid } = responseData;
const { uid, paymentUid } = responseData;
if (paymentUid) {
return await router.push(
createPaymentLink({
@@ -138,17 +138,10 @@ const BookingPage = ({
return router.push({
pathname: "/success",
query: {
date,
type: eventType.id,
eventSlug: eventType.slug,
username: profile.slug,
reschedule: !!rescheduleUid,
name: bookingForm.getValues("name"),
email: bookingForm.getValues("email"),
location: responseData.location,
eventName: profile.eventName || "",
bookingId: id,
uid,
isSuccessBookingPage: true,
email: bookingForm.getValues("email"),
eventTypeSlug: eventType.slug,
},
});
},
@@ -156,31 +149,15 @@ const BookingPage = ({
const recurringMutation = useMutation(createRecurringBooking, {
onSuccess: async (responseData = []) => {
const { attendees = [], id, recurringEventId } = responseData[0] || {};
const location = (function humanReadableLocation(location) {
if (!location) {
return;
}
if (location.includes("integration")) {
return t("web_conferencing_details_to_follow");
}
return location;
})(responseData[0].location);
const { uid } = responseData[0] || {};
return router.push({
pathname: "/success",
query: {
date,
type: eventType.id,
eventSlug: eventType.slug,
recur: recurringEventId,
username: profile.slug,
reschedule: !!rescheduleUid,
name: attendees[0].name,
email: attendees[0].email,
location,
eventName: profile.eventName || "",
bookingId: id,
uid,
allRemainingBookings: true,
email: bookingForm.getValues("email"),
eventTypeSlug: eventType.slug,
},
});
},
@@ -515,7 +492,7 @@ const BookingPage = ({
<p key={key}>{timeFormatted}</p>
))}>
<p className="dark:text-darkgray-600 text-sm">
{t("plus_more", { count: recurringStrings.length - 5 })}
+ {t("plus_more", { count: recurringStrings.length - 5 })}
</p>
</Tooltip>
</div>
@@ -14,7 +14,7 @@ import {
import { getMessageForOrganizer } from "@calcom/app-store/locations";
import { getHumanReadableLocationValue } from "@calcom/app-store/locations";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
@@ -26,7 +26,7 @@ import { QueryCell } from "@lib/QueryCell";
import CheckboxField from "@components/ui/form/CheckboxField";
import Select from "@components/ui/form/Select";
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
type OptionTypeBase = {
label: string;
@@ -81,7 +81,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
setSelectedLocation,
} = props;
const { t } = useLocale();
const locationsQuery = trpc.useQuery(["viewer.locationOptions"]);
const locationsQuery = trpc.viewer.locationOptions.useQuery();
useEffect(() => {
if (selection) {
@@ -20,11 +20,11 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
const [rescheduleReason, setRescheduleReason] = useState("");
const { mutate: rescheduleApi, isLoading } = trpc.useMutation("viewer.bookings.requestReschedule", {
const { mutate: rescheduleApi, isLoading } = trpc.viewer.bookings.requestReschedule.useMutation({
async onSuccess() {
showToast(t("reschedule_request_sent"), "success");
setIsOpenDialog(false);
await utils.invalidateQueries(["viewer.bookings"]);
await utils.viewer.bookings.invalidate();
},
onError() {
showToast(t("unexpected_error_try_again"), "error");
@@ -61,7 +61,7 @@ const AvailabilitySelect = ({
onBlur: () => void;
onChange: (value: AvailabilityOption | null) => void;
}) => {
const { data, isLoading } = trpc.useQuery(["viewer.availability.list"]);
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
if (isLoading) {
return <SelectSkeletonLoader />;
}
@@ -183,7 +183,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
};
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.useQuery(["viewer.availability.schedule", { scheduleId }]);
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId });
const filterDays = (dayNum: number) =>
schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
@@ -95,7 +95,7 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
return () => subscription.unsubscribe();
}, [watch, setValue]);
const createMutation = trpc.useMutation("viewer.eventTypes.create", {
const createMutation = trpc.viewer.eventTypes.create.useMutation({
onSuccess: async ({ eventType }) => {
await router.replace("/event-types/" + eventType.id);
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
@@ -318,13 +318,10 @@ function CreateEventTeamsItem(props: {
option: EventTypeParent;
}) {
const session = useSession();
const membershipQuery = trpc.useQuery([
"viewer.teams.getMembershipbyUser",
{
memberId: session.data?.user.id as number,
teamId: props.option.teamId as number,
},
]);
const membershipQuery = trpc.viewer.teams.getMembershipbyUser.useQuery({
memberId: session.data?.user.id as number,
teamId: props.option.teamId as number,
});
const isDisabled = membershipQuery.data?.role === "MEMBER";
@@ -15,7 +15,7 @@ import {
LocationObject,
} from "@calcom/app-store/locations";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui/Icon";
import { Button } from "@calcom/ui/components";
import { Label, Form } from "@calcom/ui/components/form";
@@ -28,7 +28,7 @@ import { QueryCell } from "@lib/QueryCell";
import { LinkText } from "@components/ui/LinkText";
import CheckboxField from "@components/ui/form/CheckboxField";
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
type OptionTypeBase = {
label: string;
@@ -83,7 +83,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
setSelectedLocation,
} = props;
const { t } = useLocale();
const locationsQuery = trpc.useQuery(["viewer.locationOptions"]);
const locationsQuery = trpc.viewer.locationOptions.useQuery();
useEffect(() => {
if (selection) {
@@ -14,7 +14,7 @@ import { Icon } from "@calcom/ui";
import { Checkbox, Button, TextField, Label } from "@calcom/ui/components";
import { CustomInputItem, Dialog, DialogContent, SettingsToggle, showToast, Tooltip } from "@calcom/ui/v2";
import CustomInputTypeForm from "@components/v2/eventtype/CustomInputTypeForm";
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
const generateHashedLink = (id: number) => {
const translator = short();
@@ -24,7 +24,7 @@ const generateHashedLink = (id: number) => {
};
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered, "eventType" | "team">) => {
const connectedCalendarsQuery = trpc.useQuery(["viewer.connectedCalendars"]);
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
const formMethods = useFormContext<FormValues>();
const { t } = useLocale();
const [showEventNameTip, setShowEventNameTip] = useState(false);
@@ -74,6 +74,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
defaultValue={eventType.destinationCalendar || undefined}
render={({ field: { onChange, value } }) => (
<DestinationCalendarSelector
destinationCalendar={eventType.destinationCalendar}
value={value ? value.externalId : undefined}
onChange={onChange}
hidePlaceholder
@@ -7,7 +7,7 @@ import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated";
import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
import { EventTypeAppsList } from "@calcom/app-store/utils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui";
import ErrorBoundary from "@calcom/ui/ErrorBoundary";
import { Button } from "@calcom/ui/components";
@@ -22,7 +22,7 @@ function AppCardWrapper({
getAppData,
setAppData,
}: {
app: inferQueryOutput<"viewer.apps">[number];
app: RouterOutputs["viewer"]["apps"][number];
eventType: EventType;
getAppData: GetAppData;
setAppData: SetAppData;
@@ -44,12 +44,9 @@ function AppCardWrapper({
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
const { t } = useLocale();
const { data: eventTypeApps, isLoading } = trpc.useQuery([
"viewer.apps",
{
extendsFeature: "EventType",
},
]);
const { data: eventTypeApps, isLoading } = trpc.viewer.apps.useQuery({
extendsFeature: "EventType",
});
const methods = useFormContext<FormValues>();
const installedApps = eventTypeApps?.filter((app) => app.credentials.length);
const notInstalledApps = eventTypeApps?.filter((app) => !app.credentials.length);
@@ -1,23 +1,29 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { useFormContext, Controller, useWatch } from "react-hook-form";
import { useMemo, useRef, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { classNames } from "@calcom/lib";
import convertToNewDurationType, { DurationType } from "@calcom/lib/convertToNewDurationType";
import findDurationType from "@calcom/lib/findDurationType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { PeriodType } from "@calcom/prisma/client";
import type { BookingLimit } from "@calcom/types/Calendar";
import { Icon } from "@calcom/ui";
import { Button } from "@calcom/ui/components";
import { Label, Input, MinutesField } from "@calcom/ui/components/form";
import { Input, InputField, Label } from "@calcom/ui/components/form";
import { Select, SettingsToggle } from "@calcom/ui/v2";
import DateRangePicker from "@calcom/ui/v2/core/form/date-range-picker/DateRangePicker";
export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">) => {
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "eventType">) => {
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const { eventType } = props;
const minimumBookingNoticeType = useRef<DurationType>(findDurationType(eventType.minimumBookingNotice));
const prevBookingNoticeType = useRef<DurationType>(minimumBookingNoticeType.current);
const minimumBookingNoticeInDurationTypeFormValue = formMethods.watch("minimumBookingNoticeInDurationType");
const PERIOD_TYPES = [
{
type: "ROLLING" as const,
@@ -47,6 +53,57 @@ export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">)
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 (
<div>
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
@@ -116,13 +173,54 @@ export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">)
</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="w-full">
<MinutesField
required
label={t("minimum_booking_notice")}
type="number"
placeholder="120"
{...formMethods.register("minimumBookingNotice", { valueAsNumber: true })}
<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="mr-2 mb-0 h-[38px] rounded-[4px]"
{...formMethods.register("minimumBookingNoticeInDurationType", {
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
onMinimumNoticeChange(event.target.value),
})}
/>
<input type="hidden" {...formMethods.register("minimumBookingNotice")} />
</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 === minimumBookingNoticeType.current
)}
onChange={(input) => onMinimumNoticeDurationTypeChange(input?.value as DurationType)}
options={durationTypeOptions}
/>
</>
);
}}
/>
</div>
<div className="w-full">
@@ -136,7 +234,7 @@ export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">)
label: t("slot_interval_default"),
value: -1,
},
...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
@@ -16,7 +16,7 @@ import { Select, Skeleton } from "@calcom/ui/v2";
import { slugify } from "@lib/slugify";
import { EditLocationDialog } from "@components/v2/eventtype/EditLocationDialog";
import { EditLocationDialog } from "@components/eventtype/EditLocationDialog";
type OptionTypeBase = {
label: string;
@@ -115,9 +115,9 @@ function EventTypeSingleLayout({
const hasPermsToDelete = currentUserMembership?.role !== "MEMBER" || !currentUserMembership;
const deleteMutation = trpc.useMutation("viewer.eventTypes.delete", {
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => {
await utils.invalidateQueries(["viewer.eventTypes"]);
await utils.viewer.eventTypes.invalidate();
showToast(t("event_type_deleted_successfully"), "success");
await router.push("/event-types");
setDeleteDialogOpen(false);
@@ -1,13 +1,12 @@
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
import { Icon } from "@calcom/ui/Icon";
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui/v2";
/** @deprecated Use `apps/web/components/v2/eventtype/SkeletonLoader.tsx` */
function SkeletonLoader() {
return (
<SkeletonContainer>
<div className="mb-4 flex items-center">
<SkeletonAvatar width="8" height="8" />
<div className="space-y-1">
<SkeletonAvatar className="h-8 w-8" />
<div className="flex flex-col space-y-1">
<SkeletonText className="h-4 w-16" />
<SkeletonText className="h-4 w-24" />
</div>
@@ -28,17 +27,17 @@ function SkeletonItem() {
<li className="group flex w-full items-center justify-between px-4 py-4 sm:px-6">
<div className="flex-grow truncate text-sm">
<div>
<SkeletonText width="32" height="5" />
<SkeletonText className="h-5 w-32" />
</div>
<div className="">
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
<li className="flex items-center whitespace-nowrap">
<Icon.FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<SkeletonText width="12" height="4" />
<SkeletonText className="h-4 w-12" />
</li>
<li className="flex items-center whitespace-nowrap">
<Icon.FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<SkeletonText width="16" height="4" />
<SkeletonText className="h-4 w-12" />
</li>
</ul>
</div>
@@ -58,7 +58,7 @@ const CalendarSwitch = (props: ICalendarSwitchProps) => {
},
{
async onSettled() {
await utils.invalidateQueries(["viewer.integrations"]);
await utils.viewer.integrations.invalidate();
},
onError() {
showToast(`Something went wrong when toggling "${title}""`, "error");
@@ -1,16 +1,16 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferMutationInput, trpc } from "@calcom/trpc/react";
import { RouterInputs, trpc } from "@calcom/trpc/react";
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
interface ICreateEventsOnCalendarSelectProps {
calendar?: inferMutationInput<"viewer.setDestinationCalendar"> | null;
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
}
const CreateEventsOnCalendarSelect = (props: ICreateEventsOnCalendarSelectProps) => {
const { calendar } = props;
const { t } = useLocale();
const mutation = trpc.useMutation(["viewer.setDestinationCalendar"]);
const mutation = trpc.viewer.setDestinationCalendar.useMutation();
return (
<>
@@ -16,12 +16,9 @@ interface IConnectCalendarsProps {
const ConnectedCalendars = (props: IConnectCalendarsProps) => {
const { nextStep } = props;
const queryConnectedCalendars = trpc.useQuery(["viewer.connectedCalendars"]);
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery();
const { t } = useLocale();
const queryIntegrations = trpc.useQuery([
"viewer.integrations",
{ variant: "calendar", onlyInstalled: false },
]);
const queryIntegrations = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
const firstCalendar = queryConnectedCalendars.data?.connectedCalendars.find(
(item) => item.calendars && item.calendars?.length > 0
@@ -24,9 +24,12 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const router = useRouter();
let queryAvailability;
if (defaultScheduleId) {
queryAvailability = trpc.useQuery(["viewer.availability.schedule", { scheduleId: defaultScheduleId }], {
enabled: router.isReady,
});
queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: defaultScheduleId },
{
enabled: router.isReady,
}
);
}
const availabilityForm = useForm({
@@ -43,8 +46,8 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
nextStep();
},
};
const createSchedule = trpc.useMutation("viewer.availability.schedule.create", mutationOptions);
const updateSchedule = trpc.useMutation("viewer.availability.schedule.update", mutationOptions);
const createSchedule = trpc.viewer.availability.schedule.create.useMutation(mutationOptions);
const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions);
return (
<Form
className="w-full bg-white text-black dark:bg-opacity-5 dark:text-white"
@@ -32,17 +32,17 @@ const UserProfile = (props: IUserProfile) => {
handleSubmit,
formState: { errors },
} = useForm<FormData>({ defaultValues: { bio: user?.bio || "" } });
const { data: eventTypes } = trpc.useQuery(["viewer.eventTypes.list"]);
const { data: eventTypes } = trpc.viewer.eventTypes.list.useQuery();
const [imageSrc, setImageSrc] = useState<string>(user?.avatar || "");
const utils = trpc.useContext();
const router = useRouter();
const createEventType = trpc.useMutation("viewer.eventTypes.create");
const createEventType = trpc.viewer.eventTypes.create.useMutation();
const mutation = trpc.useMutation("viewer.updateProfile", {
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async (_data, context) => {
if (context.avatar) {
showToast(t("your_user_profile_updated_successfully"), "success");
await utils.refetchQueries(["viewer.me"]);
await utils.viewer.me.refetch();
} else {
try {
if (eventTypes?.length === 0) {
@@ -56,7 +56,7 @@ const UserProfile = (props: IUserProfile) => {
console.error(error);
}
await utils.refetchQueries(["viewer.me"]);
await utils.viewer.me.refetch();
router.push("/");
}
},
@@ -36,13 +36,13 @@ const UserSettings = (props: IUserSettingsProps) => {
const utils = trpc.useContext();
const onSuccess = async () => {
await utils.invalidateQueries(["viewer.me"]);
await utils.viewer.me.invalidate();
nextStep();
};
const mutation = trpc.useMutation("viewer.updateProfile", {
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: onSuccess,
});
const { data: stripeCustomer } = trpc.useQuery(["viewer.stripeCustomer"]);
const { data: stripeCustomer } = trpc.viewer.stripeCustomer.useQuery();
const paymentRequired = stripeCustomer?.isPremium ? !stripeCustomer?.paidForPremium : false;
const onSubmit = handleSubmit((data) => {
if (paymentRequired) {
@@ -3,13 +3,13 @@ import { Fragment } from "react";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { List } from "@calcom/ui/List";
import { ShellSubHeading } from "@calcom/ui/Shell";
import Switch from "@calcom/ui/Switch";
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
import { Button } from "@calcom/ui/components/button";
import showToast from "@calcom/ui/v2/core/notifications";
import { QueryCell } from "@lib/QueryCell";
@@ -73,7 +73,7 @@ function CalendarSwitch(props: {
},
{
async onSettled() {
await utils.invalidateQueries(["viewer.integrations"]);
await utils.viewer.integrations.invalidate();
},
onError() {
showToast(`Something went wrong when toggling "${props.title}""`, "error");
@@ -97,7 +97,7 @@ function CalendarSwitch(props: {
function CalendarList(props: Props) {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.integrations", { variant: "calendar", onlyInstalled: false }]);
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
return (
<QueryCell
@@ -133,7 +133,10 @@ function CalendarList(props: Props) {
function ConnectedCalendarsList(props: Props) {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.connectedCalendars"], { suspense: true });
const query = trpc.viewer.connectedCalendars.useQuery(undefined, {
suspense: true,
trpc: {},
});
const { fromOnboarding } = props;
return (
<QueryCell
@@ -158,7 +161,7 @@ function ConnectedCalendarsList(props: Props) {
id={item.credentialId}
externalId={item.primary?.externalId}
render={(btnProps) => (
<Button {...btnProps} color="warn" data-testid="integration-connection-button">
<Button {...btnProps} color="secondary" data-testid="integration-connection-button">
{t("disconnect")}
</Button>
)}
@@ -191,7 +194,7 @@ function ConnectedCalendarsList(props: Props) {
<DisconnectIntegration
id={item.credentialId}
render={(btnProps) => (
<Button {...btnProps} color="warn" data-testid="integration-connection-button">
<Button {...btnProps} color="secondary" data-testid="integration-connection-button">
Disconnect
</Button>
)}
@@ -211,7 +214,7 @@ function ConnectedCalendarsList(props: Props) {
export function CalendarListContainer(props: {
heading?: boolean;
items?: inferQueryOutput<"viewer.integrations">["items"];
items?: RouterOutputs["viewer"]["integrations"]["items"];
fromOnboarding?: boolean;
}) {
const { t } = useLocale();
@@ -219,17 +222,17 @@ export function CalendarListContainer(props: {
const utils = trpc.useContext();
const onChanged = () =>
Promise.allSettled([
utils.invalidateQueries(["viewer.integrations", { variant: "calendar", onlyInstalled: true }], {
exact: true,
}),
utils.invalidateQueries(["viewer.connectedCalendars"]),
utils.viewer.integrations.invalidate(
{ variant: "calendar", onlyInstalled: true },
{
exact: true,
}
),
utils.viewer.connectedCalendars.invalidate(),
]);
const query = trpc.useQuery(["viewer.connectedCalendars"]);
const installedCalendars = trpc.useQuery([
"viewer.integrations",
{ variant: "calendar", onlyInstalled: true },
]);
const mutation = trpc.useMutation("viewer.setDestinationCalendar");
const query = trpc.viewer.connectedCalendars.useQuery();
const installedCalendars = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
const mutation = trpc.viewer.setDestinationCalendar.useMutation();
return (
<QueryCell
query={query}
@@ -2,9 +2,9 @@ import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { ButtonBaseProps } from "@calcom/ui/Button";
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
import { Dialog } from "@calcom/ui/Dialog";
import { ButtonBaseProps } from "@calcom/ui/components/button";
import showToast from "@calcom/ui/v2/core/notifications";
export default function DisconnectIntegration(props: {
@@ -18,7 +18,7 @@ export default function DisconnectIntegration(props: {
const { t } = useLocale();
const [modalOpen, setModalOpen] = useState(false);
const mutation = trpc.useMutation("viewer.deleteCredential", {
const mutation = trpc.viewer.deleteCredential.useMutation({
onSettled: async () => {
await props.onOpenChange(modalOpen);
},
@@ -1,69 +0,0 @@
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { ButtonBaseProps } from "@calcom/ui/Button";
import { Dialog } from "@calcom/ui/Dialog";
import showToast from "@calcom/ui/v2/core/notifications";
import DeleteStripeDialogContent from "@components/dialog/DeleteStripeDialogContent";
export default function DisconnectStripeIntegration(props: {
/** Integration credential id */
id: number;
render: (renderProps: ButtonBaseProps) => JSX.Element;
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
}) {
const [modalOpen, setModalOpen] = useState(false);
const mutation = useMutation(
async (action: string) => {
const res = await fetch("/api/integrations", {
method: "DELETE",
body: JSON.stringify({ id: props.id, action }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error("Something went wrong");
}
return res.json();
},
{
async onSettled() {
await props.onOpenChange(modalOpen);
},
onSuccess(data) {
showToast(data.message, "success");
setModalOpen(false);
},
}
);
return (
<>
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DeleteStripeDialogContent
variety="warning"
title="Disconnect Stripe Integration"
cancelAllBookingsBtnText="Cancel all bookings"
removeBtnText="Remove payment"
cancelBtnText="Cancel"
onConfirm={() => {
mutation.mutate("cancel");
}}
onRemove={() => {
mutation.mutate("remove");
}}>
If you have unpaid and unconfirmed bookings, you must choose to cancel them or remove the required
payment field.
</DeleteStripeDialogContent>
</Dialog>
{props.render({
onClick() {
setModalOpen(true);
},
disabled: modalOpen,
loading: mutation.isLoading,
})}
</>
);
}
@@ -9,13 +9,13 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
const { t } = useLocale();
const mutation = trpc.useMutation("viewer.updateProfile", {
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async () => {
showToast(t("your_user_profile_updated_successfully"), "success");
await utils.invalidateQueries(["viewer.me"]);
await utils.viewer.me.invalidate();
},
async onSettled() {
await utils.invalidateQueries(["viewer.public.i18n"]);
await utils.viewer.public.i18n.invalidate();
},
});
@@ -57,7 +57,7 @@ export function CalendarSwitch(props: {
},
{
async onSettled() {
await utils.invalidateQueries(["viewer.integrations"]);
await utils.viewer.integrations.invalidate();
},
onError() {
showToast(`Something went wrong when toggling "${props.title}""`, "error");
@@ -1,62 +0,0 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Badge } from "@calcom/ui/components/badge";
import showToast from "@calcom/ui/v2/core/notifications";
/** @deprecated Use `packages/features/ee/teams/components/DisableTeamImpersonation.tsx` */
const DisableTeamImpersonation = ({ teamId, memberId }: { teamId: number; memberId: number }) => {
const { t } = useLocale();
const utils = trpc.useContext();
const query = trpc.useQuery(["viewer.teams.getMembershipbyUser", { teamId, memberId }]);
const mutation = trpc.useMutation("viewer.teams.updateMembership", {
onSuccess: async () => {
showToast(t("your_user_profile_updated_successfully"), "success");
await utils.invalidateQueries(["viewer.teams.getMembershipbyUser"]);
},
async onSettled() {
await utils.invalidateQueries(["viewer.public.i18n"]);
},
});
if (query.isLoading) return <></>;
return (
<>
<h3 className="font-cal mt-7 pb-4 text-xl leading-6 text-gray-900">{t("settings")}</h3>
<div className="-mx-0 rounded-sm border border-neutral-200 bg-white px-4 pb-4 sm:px-6">
<div className="flex flex-col justify-between pt-4 sm:flex-row">
<div>
<div className="flex flex-row items-center">
<h2 className="font-cal font-bold leading-6 text-gray-900">
{t("user_impersonation_heading")}
</h2>
<Badge
className="ml-2 text-xs"
variant={!query.data?.disableImpersonation ? "success" : "gray"}>
{!query.data?.disableImpersonation ? t("enabled") : t("disabled")}
</Badge>
</div>
<p className="text-sm text-gray-700">{t("team_impersonation_description")}</p>
</div>
<div className="mt-5 sm:mt-0 sm:self-center">
<Button
type="submit"
color="secondary"
onClick={() =>
!query.data?.disableImpersonation
? mutation.mutate({ teamId, memberId, disableImpersonation: true })
: mutation.mutate({ teamId, memberId, disableImpersonation: false })
}>
{!query.data?.disableImpersonation ? t("disable") : t("enable")}
</Button>
</div>
</div>
</div>
</>
);
};
export default DisableTeamImpersonation;
@@ -1,115 +0,0 @@
import { MembershipRole } from "@prisma/client";
import { SyntheticEvent, useMemo, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import ModalContainer from "@components/ui/ModalContainer";
import Select from "@components/ui/form/Select";
type MembershipRoleOption = {
label: string;
value: MembershipRole;
};
/** @deprecated Use `packages/features/ee/teams/components/MemberChangeRoleModal.tsx` */
export default function MemberChangeRoleModal(props: {
isOpen: boolean;
currentMember: MembershipRole;
memberId: number;
teamId: number;
initialRole: MembershipRole;
onExit: () => void;
}) {
const { t } = useLocale();
const options = useMemo(() => {
return [
{
label: t("member"),
value: MembershipRole.MEMBER,
},
{
label: t("admin"),
value: MembershipRole.ADMIN,
},
{
label: t("owner"),
value: MembershipRole.OWNER,
},
].filter(({ value }) => value !== MembershipRole.OWNER || props.currentMember === MembershipRole.OWNER);
}, [t, props.currentMember]);
const [role, setRole] = useState<MembershipRoleOption>(
options.find((option) => option.value === props.initialRole) || {
label: t("member"),
value: MembershipRole.MEMBER,
}
);
const [errorMessage, setErrorMessage] = useState("");
const utils = trpc.useContext();
const changeRoleMutation = trpc.useMutation("viewer.teams.changeMemberRole", {
async onSuccess() {
await utils.invalidateQueries(["viewer.teams.get"]);
props.onExit();
},
async onError(err) {
setErrorMessage(err.message);
},
});
function changeRole(e: SyntheticEvent) {
e.preventDefault();
changeRoleMutation.mutate({
teamId: props.teamId,
memberId: props.memberId,
role: role.value,
});
}
return (
<ModalContainer isOpen={props.isOpen} onExit={props.onExit}>
<>
<div className="mb-4 sm:flex sm:items-start">
<div className="text-center sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{t("change_member_role")}
</h3>
</div>
</div>
<form onSubmit={changeRole}>
<div className="mb-4">
<label className="mb-2 block text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
{t("role")}
</label>
{/*<option value="OWNER">{t("owner")}</option> - needs dialog to confirm change of ownership */}
<Select
isSearchable={false}
options={options}
value={role}
onChange={(option) => option && setRole(option)}
id="role"
className="mt-1 block w-full rounded-md border-gray-300 text-sm"
/>
</div>
{errorMessage && (
<p className="text-sm text-red-700">
<span className="font-bold">Error: </span>
{errorMessage}
</p>
)}
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button type="submit" color="primary" className="ltr:ml-2 rtl:mr-2">
{t("save")}
</Button>
<Button type="button" color="secondary" onClick={props.onExit}>
{t("cancel")}
</Button>
</div>
</form>
</>
</ModalContainer>
);
}
@@ -1,159 +0,0 @@
import { MembershipRole } from "@prisma/client";
import React, { useState, SyntheticEvent, useMemo } from "react";
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
import { TextField } from "@calcom/ui/form/fields";
import { useLocale } from "@lib/hooks/useLocale";
import Select from "@components/ui/form/Select";
type MemberInvitationModalProps = {
isOpen: boolean;
team: TeamWithMembers | null;
currentMember: MembershipRole;
onExit: () => void;
};
type MembershipRoleOption = {
value: MembershipRole;
label?: string;
};
const _options: MembershipRoleOption[] = [{ value: "MEMBER" }, { value: "ADMIN" }, { value: "OWNER" }];
/** @deprecated Use `packages/features/ee/teams/components/MemberInvitationModal.tsx` */
export default function MemberInvitationModal(props: MemberInvitationModalProps) {
const [errorMessage, setErrorMessage] = useState("");
const { t, i18n } = useLocale();
const utils = trpc.useContext();
const options = useMemo(() => {
_options.forEach((option, i) => {
_options[i].label = t(option.value.toLowerCase());
});
return _options;
}, [t]);
const inviteMemberMutation = trpc.useMutation("viewer.teams.inviteMember", {
async onSuccess() {
await utils.invalidateQueries(["viewer.teams.get"]);
props.onExit();
},
async onError(err) {
setErrorMessage(err.message);
},
});
function inviteMember(e: SyntheticEvent) {
e.preventDefault();
if (!props.team) return;
const target = e.target as typeof e.target & {
elements: {
role: { value: MembershipRole };
inviteUser: { value: string };
sendInviteEmail: { checked: boolean };
};
};
inviteMemberMutation.mutate({
teamId: props.team.id,
language: i18n.language,
role: target.elements["role"].value,
usernameOrEmail: target.elements["inviteUser"].value,
sendEmailInvitation: target.elements["sendInviteEmail"].checked,
});
}
return (
<Dialog open={props.isOpen} onOpenChange={props.onExit}>
<DialogContent>
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<Icon.FiUser className="text-brandcontrast h-6 w-6" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{t("invite_new_member")}
</h3>
<div>
<p className="text-sm text-gray-400">{t("invite_new_team_member")}</p>
</div>
</div>
</div>
<form onSubmit={inviteMember}>
<div className="space-y-4">
<TextField
label={t("email_or_username")}
id="inviteUser"
name="inviteUser"
placeholder="email@example.com"
required
/>
<div>
<label className="mb-1 block text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
{t("role")}
</label>
<Select
defaultValue={options[0]}
options={props.currentMember !== MembershipRole.OWNER ? options.slice(0, 2) : options}
id="role"
name="role"
className="mt-1 block w-full rounded-sm border-gray-300 text-sm"
/>
</div>
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<input
type="checkbox"
name="sendInviteEmail"
defaultChecked
id="sendInviteEmail"
className="rounded-sm border-gray-300 text-sm text-black"
/>
</div>
<div className="text-sm ltr:ml-2 rtl:mr-2">
<label htmlFor="sendInviteEmail" className="font-medium text-gray-700">
{t("send_invite_email")}
</label>
</div>
</div>
<div className="flex flex-row rounded-sm bg-gray-50 px-3 py-2">
<Icon.FiInfo className="h-5 w-5 flex-shrink-0 fill-gray-400" aria-hidden="true" />
<span className="ml-2 text-sm leading-tight text-gray-500">
Note: This will cost an extra seat ($12/m) on your subscription if this invitee does not have
a pro account.{" "}
<a href="#" className="underline">
Learn More
</a>
</span>
</div>
</div>
{errorMessage && (
<p className="text-sm text-red-700">
<span className="font-bold">Error: </span>
{errorMessage}
</p>
)}
<DialogFooter>
<Button type="button" color="secondary" onClick={props.onExit}>
{t("cancel")}
</Button>
<Button
type="submit"
color="primary"
className="ltr:ml-2 rtl:mr-2"
data-testid="invite-new-member-button">
{t("invite")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
-22
View File
@@ -1,22 +0,0 @@
import { inferQueryOutput } from "@calcom/trpc/react";
import MemberListItem from "./MemberListItem";
interface Props {
team: inferQueryOutput<"viewer.teams.get">;
members: inferQueryOutput<"viewer.teams.get">["members"];
}
export default function MemberList(props: Props) {
if (!props.members.length) return <></>;
return (
<div>
<ul className="-mx-4 mb-2 divide-y divide-gray-200 rounded border bg-white px-4 sm:mx-0 sm:px-4">
{props.members?.map((member) => (
<MemberListItem key={member.id} member={member} team={props.team} />
))}
</ul>
</div>
);
}
-260
View File
@@ -1,260 +0,0 @@
import { MembershipRole } from "@prisma/client";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { useState } from "react";
import TeamAvailabilityModal from "@calcom/features/ee/teams/components/TeamAvailabilityModal";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@calcom/ui/Dropdown";
import { Icon } from "@calcom/ui/Icon";
import { Tooltip } from "@calcom/ui/Tooltip";
import showToast from "@calcom/ui/v2/core/notifications";
import useCurrentUserId from "@lib/hooks/useCurrentUserId";
import Avatar from "@components/ui/Avatar";
import ModalContainer from "@components/ui/ModalContainer";
import MemberChangeRoleModal from "./MemberChangeRoleModal";
import TeamPill, { TeamRole } from "./TeamPill";
interface Props {
team: inferQueryOutput<"viewer.teams.get">;
member: inferQueryOutput<"viewer.teams.get">["members"][number];
}
/** @deprecated Use `packages/features/ee/teams/components/MemberListItem.tsx` */
export default function MemberListItem(props: Props) {
const { t } = useLocale();
const utils = trpc.useContext();
const [showChangeMemberRoleModal, setShowChangeMemberRoleModal] = useState(false);
const [showTeamAvailabilityModal, setShowTeamAvailabilityModal] = useState(false);
const [showImpersonateModal, setShowImpersonateModal] = useState(false);
const removeMemberMutation = trpc.useMutation("viewer.teams.removeMember", {
async onSuccess() {
await utils.invalidateQueries(["viewer.teams.get"]);
showToast(t("success"), "success");
},
async onError(err) {
showToast(err.message, "error");
},
});
const ownersInTeam = () => {
const { members } = props.team;
const owners = members.filter((member) => member["role"] === MembershipRole.OWNER && member["accepted"]);
return owners.length;
};
const currentUserId = useCurrentUserId();
const name =
props.member.name ||
(() => {
const emailName = props.member.email.split("@")[0];
return emailName.charAt(0).toUpperCase() + emailName.slice(1);
})();
const removeMember = () =>
removeMemberMutation.mutate({ teamId: props.team?.id, memberId: props.member.id });
return (
<li className="divide-y">
<div className="my-4 flex justify-between">
<div className="flex w-full flex-col justify-between sm:flex-row">
<div className="flex">
<Avatar
imageSrc={WEBAPP_URL + "/" + props.member.username + "/avatar.png"}
alt={name || ""}
className="h-9 w-9 rounded-full"
/>
<div className="ml-3 inline-block">
<span className="text-sm font-bold text-neutral-700">{name}</span>
<span
className="-mt-1 block text-xs text-gray-400"
data-testid="member-email"
data-email={props.member.email}>
{props.member.email}
</span>
</div>
</div>
<div className="mt-2 flex ltr:mr-2 rtl:ml-2 sm:mt-0 sm:justify-center">
{/* Tooltip doesn't show... WHY????? */}
{props.member.isMissingSeat && (
<Tooltip side="top" content={t("hidden_team_member_message")}>
<TeamPill color="red" text={t("hidden")} />
</Tooltip>
)}
{!props.member.accepted && <TeamPill color="yellow" text={t("invitee")} />}
{props.member.role && <TeamRole role={props.member.role} />}
</div>
</div>
<div className="flex space-x-2">
<Tooltip side="top" content={t("team_view_user_availability")}>
<Button
// Disabled buttons don't trigger Tooltips
title={
props.member.accepted
? t("team_view_user_availability")
: t("team_view_user_availability_disabled")
}
disabled={!props.member.accepted}
onClick={() => (props.member.accepted ? setShowTeamAvailabilityModal(true) : null)}
color="minimal"
size="icon">
<Icon.FiClock className="h-5 w-5 group-hover:text-gray-800" />
</Button>
</Tooltip>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button type="button" color="minimal" size="icon" StartIcon={Icon.FiMoreHorizontal} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Link href={"/" + props.member.username}>
<a target="_blank">
<Button color="minimal" StartIcon={Icon.FiExternalLink} className="w-full font-normal">
{t("view_public_page")}
</Button>
</a>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
{((props.team.membership.role === MembershipRole.OWNER &&
(props.member.role !== MembershipRole.OWNER ||
ownersInTeam() > 1 ||
props.member.id !== currentUserId)) ||
(props.team.membership.role === MembershipRole.ADMIN &&
props.member.role !== MembershipRole.OWNER)) && (
<>
<DropdownMenuItem>
<Button
onClick={() => setShowChangeMemberRoleModal(true)}
color="minimal"
StartIcon={Icon.FiEdit2}
className="w-full flex-shrink-0 font-normal">
{t("edit_role")}
</Button>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
{/* Only show impersonate box if - The user has impersonation enabled,
They have accepted the team invite, and it is enabled for this instance */}
{!props.member.disableImpersonation &&
props.member.accepted &&
process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "true" && (
<>
<DropdownMenuItem>
<Button
onClick={() => setShowImpersonateModal(true)}
color="minimal"
StartIcon={Icon.FiLock}
className="w-full flex-shrink-0 font-normal">
{t("impersonate")}
</Button>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
</>
)}
<DropdownMenuItem>
<Dialog>
<DialogTrigger asChild>
<Button
onClick={(e) => {
e.stopPropagation();
}}
color="warn"
StartIcon={Icon.FiUserMinus}
className="w-full font-normal">
{t("remove_member")}
</Button>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("remove_member")}
confirmBtnText={t("confirm_remove_member")}
onConfirm={removeMember}>
{t("remove_member_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</Dropdown>
</div>
</div>
{showChangeMemberRoleModal && (
<MemberChangeRoleModal
isOpen={showChangeMemberRoleModal}
currentMember={props.team.membership.role}
teamId={props.team?.id}
memberId={props.member.id}
initialRole={props.member.role as MembershipRole}
onExit={() => setShowChangeMemberRoleModal(false)}
/>
)}
{showImpersonateModal && props.member.username && (
<ModalContainer isOpen={showImpersonateModal} onExit={() => setShowImpersonateModal(false)}>
<>
<div className="mb-4 sm:flex sm:items-start">
<div className="text-center sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{t("impersonate")}
</h3>
</div>
</div>
<form
onSubmit={async (e) => {
e.preventDefault();
await signIn("impersonation-auth", {
username: props.member.username,
teamId: props.team.id,
});
}}>
<p className="mt-2 text-sm text-gray-500" id="email-description">
{t("impersonate_user_tip")}
</p>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button type="submit" color="primary" className="ltr:ml-2 rtl:mr-2">
{t("impersonate")}
</Button>
<Button type="button" color="secondary" onClick={() => setShowImpersonateModal(false)}>
{t("cancel")}
</Button>
</div>
</form>
</>
</ModalContainer>
)}
{showTeamAvailabilityModal && (
<ModalContainer
wide
noPadding
isOpen={showTeamAvailabilityModal}
onExit={() => setShowTeamAvailabilityModal(false)}>
<TeamAvailabilityModal team={props.team} member={props.member} />
<div className="space-x-2 border-t py-5 rtl:space-x-reverse">
<Button onClick={() => setShowTeamAvailabilityModal(false)}>{t("done")}</Button>
{props.team.membership.role !== MembershipRole.MEMBER && (
<Link href={`/settings/teams/${props.team.id}/availability`} passHref>
<Button color="secondary">{t("Open Team Availability")}</Button>
</Link>
)}
</div>
</ModalContainer>
)}
</li>
);
}
@@ -1,73 +0,0 @@
import { useRef, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import { Icon } from "@calcom/ui/Icon";
import { Alert } from "@calcom/ui/v2/core/Alert";
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/v2/core/Dialog";
interface Props {
isOpen: boolean;
onClose: () => void;
}
export default function TeamCreate(props: Props) {
const { t } = useLocale();
const utils = trpc.useContext();
const [errorMessage, setErrorMessage] = useState<null | string>(null);
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const createTeamMutation = trpc.useMutation("viewer.teams.create", {
onSuccess: () => {
utils.invalidateQueries(["viewer.teams.list"]);
props.onClose();
},
onError: (e) => {
setErrorMessage(e?.message || t("something_went_wrong"));
},
});
const createTeam = () => {
createTeamMutation.mutate({ name: nameRef?.current?.value });
};
return (
<>
<Dialog open={props.isOpen} onOpenChange={props.onClose}>
<DialogContent type="creation" actionText={t("create_new_team")} actionOnClick={createTeam}>
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<Icon.FiUsers className="text-brandcontrast h-6 w-6" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{t("create_new_team")}
</h3>
<div>
<p className="text-sm text-gray-400">{t("create_new_team_description")}</p>
</div>
</div>
</div>
<form>
<div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
{t("name")}
</label>
<input
ref={nameRef}
type="text"
name="name"
id="name"
placeholder="Acme Inc."
required
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm"
/>
</div>
{errorMessage && <Alert severity="error" title={errorMessage} />}
</form>
</DialogContent>
</Dialog>
</>
);
}
-37
View File
@@ -1,37 +0,0 @@
import { MembershipRole } from "@prisma/client";
import classNames from "classnames";
import { useLocale } from "@lib/hooks/useLocale";
type PillColor = "blue" | "green" | "red" | "yellow";
interface Props {
text: string;
color?: PillColor;
}
/** @deprecated Use `packages/features/ee/teams/components/TeamPill.tsx` */
export default function TeamPill(props: Props) {
return (
<div
className={classNames("self-center rounded-md border px-3 py-1 text-xs capitalize ltr:mr-2 rtl:ml-2", {
"border-gray-200 bg-gray-50 text-gray-700": !props.color,
"border-blue-200 bg-blue-50 text-blue-700": props.color === "blue",
"border-red-200 bg-red-50 text-red-700": props.color === "red",
"border-yellow-200 bg-yellow-50 text-yellow-700": props.color === "yellow",
"border-green-200 bg-green-50 text-green-600": props.color === "green",
})}>
{props.text}
</div>
);
}
export function TeamRole(props: { role: MembershipRole }) {
const { t } = useLocale();
const keys: Record<MembershipRole, PillColor | undefined> = {
[MembershipRole.OWNER]: undefined,
[MembershipRole.ADMIN]: "red",
[MembershipRole.MEMBER]: "blue",
};
return <TeamPill text={t(props.role.toLowerCase())} color={keys[props.role]} />;
}
+2 -2
View File
@@ -27,13 +27,13 @@ export default function TeamSettings(props: Props) {
const hasLogo = !!team?.logo;
const utils = trpc.useContext();
const mutation = trpc.useMutation("viewer.teams.update", {
const mutation = trpc.viewer.teams.update.useMutation({
onError: (err) => {
setHasErrors(true);
setErrorMessage(err.message);
},
async onSuccess() {
await utils.invalidateQueries(["viewer.teams.get"]);
await utils.viewer.teams.get.invalidate();
showToast(t("your_team_updated_successfully"), "success");
setHasErrors(false);
},
@@ -22,16 +22,16 @@ export default function TeamSettingsRightSidebar(props: { team: TeamWithMembers;
const permalink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${props.team?.slug}`;
const deleteTeamMutation = trpc.useMutation("viewer.teams.delete", {
const deleteTeamMutation = trpc.viewer.teams.delete.useMutation({
async onSuccess() {
await utils.invalidateQueries(["viewer.teams.get"]);
await utils.viewer.teams.get.invalidate();
router.push(`/settings/teams`);
showToast(t("your_team_updated_successfully"), "success");
},
});
const acceptOrLeaveMutation = trpc.useMutation("viewer.teams.acceptOrLeave", {
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
onSuccess: () => {
utils.invalidateQueries(["viewer.teams.list"]);
utils.viewer.teams.list.invalidate();
router.push(`/settings/teams`);
},
});
@@ -1,91 +0,0 @@
import { useState } from "react";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogClose,
DialogFooter,
DialogHeader,
} from "@calcom/ui/Dialog";
import showToast from "@calcom/ui/v2/core/notifications";
import { useLocale } from "@lib/hooks/useLocale";
interface Props {
teamId: number;
}
/** @deprecated Use `packages/features/ee/teams/components/UpgradeToFlexibleProModal.tsx` */
export function UpgradeToFlexibleProModal(props: Props) {
const { t } = useLocale();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const utils = trpc.useContext();
const { data } = trpc.useQuery(["viewer.teams.getTeamSeats", { teamId: props.teamId }], {
onError: (err) => {
setErrorMessage(err.message);
},
});
const mutation = trpc.useMutation(["viewer.teams.upgradeTeam"], {
onSuccess: (data) => {
// if the user does not already have a Stripe subscription, this wi
if (data?.url) {
window.location.href = data.url;
}
if (data?.success) {
utils.invalidateQueries(["viewer.teams.get"]);
showToast(t("team_upgraded_successfully"), "success");
}
},
onError: (err) => {
setErrorMessage(err.message);
},
});
return (
<Dialog
onOpenChange={() => {
setErrorMessage(null);
}}>
<DialogTrigger asChild>
<a className="cursor-pointer underline">Upgrade Now</a>
</DialogTrigger>
<DialogContent>
<DialogHeader title={t("Purchase missing seats")} />
<p className="-mt-4 text-sm text-gray-600">{t("changed_team_billing_info")}</p>
{data && (
<p className="mt-2 text-sm italic text-gray-700">
{t("team_upgrade_seats_details", {
memberCount: data.totalMembers,
unpaidCount: data.missingSeats,
seatPrice: 12,
totalCost: (data.totalMembers - data.freeSeats) * 12 + 12,
})}
</p>
)}
{errorMessage && (
<Alert severity="error" title={errorMessage} message={t("further_billing_help")} className="my-4" />
)}
<DialogFooter>
<DialogClose>
<Button color="secondary">{t("close")}</Button>
</DialogClose>
<Button
disabled={mutation.isLoading}
onClick={() => {
setErrorMessage(null);
mutation.mutate({ teamId: props.teamId });
}}>
{t("upgrade_to_per_seat")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -4,7 +4,8 @@ import { trpc } from "@calcom/trpc/react";
import NoCalendarConnectedAlert from "..";
describe("Testing NoCalendarConnectedAlert", () => {
// TODO: useQuery mock is not working
describe.skip("Testing NoCalendarConnectedAlert", () => {
describe("Render test", () => {
it("should render without crashing", () => {
// Disabled as its asking for full trpc useQuery response
@@ -10,7 +10,7 @@ import { LinkText } from "../LinkText";
const NoCalendarConnectedAlert = () => {
const { t } = useTranslation();
const query = trpc.useQuery(["viewer.connectedCalendars"]);
const query = trpc.viewer.connectedCalendars.useQuery();
// We are not gonna show this alert till we fetch data from DB
let defaultCalendarConnected = true;
if (query.isSuccess && query.isFetched && query.data) {
@@ -9,7 +9,7 @@ import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { User } from "@calcom/prisma/client";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Dialog, DialogClose, DialogContent, DialogHeader } from "@calcom/ui/Dialog";
import { Icon, StarIconSolid } from "@calcom/ui/Icon";
@@ -57,7 +57,7 @@ const obtainNewUsernameChangeCondition = ({
}: {
userIsPremium: boolean;
isNewUsernamePremium: boolean;
stripeCustomer: inferQueryOutput<"viewer.stripeCustomer"> | undefined;
stripeCustomer: RouterOutputs["viewer"]["stripeCustomer"] | undefined;
}) => {
if (!userIsPremium && isNewUsernamePremium && !stripeCustomer?.paidForPremium) {
return UsernameChangeStatusEnum.UPGRADE;
@@ -87,7 +87,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
const router = useRouter();
const { paymentStatus: recentAttemptPaymentStatus } = router.query;
const [openDialogSaveUsername, setOpenDialogSaveUsername] = useState(false);
const { data: stripeCustomer } = trpc.useQuery(["viewer.stripeCustomer"]);
const { data: stripeCustomer } = trpc.viewer.stripeCustomer.useQuery();
const isCurrentUsernamePremium =
user && user.metadata && hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false;
const [isInputUsernamePremium, setIsInputUsernamePremium] = useState(false);
@@ -116,7 +116,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
}, [debouncedApiCall, inputUsernameValue]);
const utils = trpc.useContext();
const updateUsername = trpc.useMutation("viewer.updateProfile", {
const updateUsername = trpc.viewer.updateProfile.useMutation({
onSuccess: async () => {
onSuccessMutation && (await onSuccessMutation());
setOpenDialogSaveUsername(false);
@@ -125,7 +125,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
onErrorMutation && onErrorMutation(error);
},
async onSettled() {
await utils.invalidateQueries(["viewer.public.i18n"]);
await utils.viewer.public.i18n.invalidate();
},
});
@@ -63,16 +63,17 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
const utils = trpc.useContext();
const updateUsernameMutation = trpc.useMutation("viewer.updateProfile", {
const updateUsernameMutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async () => {
onSuccessMutation && (await onSuccessMutation());
setOpenDialogSaveUsername(false);
setCurrentUsername(inputUsernameValue);
},
onError: (error) => {
onErrorMutation && onErrorMutation(error);
},
async onSettled() {
await utils.invalidateQueries(["viewer.public.i18n"]);
await utils.viewer.public.i18n.invalidate();
},
});
@@ -104,10 +105,9 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
};
const updateUsername = async () => {
await updateUsernameMutation.mutate({
updateUsernameMutation.mutate({
username: inputUsernameValue,
});
setCurrentUsername(inputUsernameValue);
};
return (
@@ -1,47 +0,0 @@
import { Icon } from "@calcom/ui/Icon";
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui/v2";
function SkeletonLoader() {
return (
<SkeletonContainer>
<div className="mb-4 flex items-center">
<SkeletonAvatar className="h-8 w-8" />
<div className="flex flex-col space-y-1">
<SkeletonText className="h-4 w-16" />
<SkeletonText className="h-4 w-24" />
</div>
</div>
<ul className="divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
</ul>
</SkeletonContainer>
);
}
export default SkeletonLoader;
function SkeletonItem() {
return (
<li className="group flex w-full items-center justify-between px-4 py-4 sm:px-6">
<div className="flex-grow truncate text-sm">
<div>
<SkeletonText className="h-5 w-32" />
</div>
<div className="">
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
<li className="flex items-center whitespace-nowrap">
<Icon.FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<SkeletonText className="h-4 w-12" />
</li>
<li className="flex items-center whitespace-nowrap">
<Icon.FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<SkeletonText className="h-4 w-16" />
</li>
</ul>
</div>
</div>
</li>
);
}
+13 -28
View File
@@ -9,18 +9,13 @@ import { ReactNode } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import type {
inferHandlerInput,
inferProcedureInput,
inferProcedureOutput,
ProcedureRecord,
} from "@calcom/trpc/server";
import type { DecorateProcedure } from "@calcom/trpc/react/shared";
import type { AnyQueryProcedure, inferProcedureInput, inferProcedureOutput } from "@calcom/trpc/server";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Alert } from "@calcom/ui/Alert";
import Loader from "@calcom/ui/Loader";
import type { UseTRPCQueryOptions } from "@trpc/react/shared";
import type { UseTRPCQueryOptions } from "@trpc/react-query/shared";
type ErrorLike = {
message: string;
@@ -87,34 +82,24 @@ export function QueryCell<TData, TError extends ErrorLike>(
return null;
}
type inferProcedures<TObj extends ProcedureRecord> = {
[TPath in keyof TObj]: {
input: inferProcedureInput<TObj[TPath]>;
output: inferProcedureOutput<TObj[TPath]>;
};
};
type TQueryValues = inferProcedures<AppRouter["_def"]["queries"]>;
type TQueries = AppRouter["_def"]["queries"];
type TError = TRPCClientErrorLike<AppRouter>;
const withQuery = <TPath extends keyof TQueryValues & string>(
pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>],
params?: UseTRPCQueryOptions<
TPath,
TQueryValues[TPath]["input"],
TQueryValues[TPath]["output"],
TQueryValues[TPath]["output"],
TError
>
const withQuery = <
TQuery extends AnyQueryProcedure,
TInput = inferProcedureInput<TQuery>,
TOutput = inferProcedureOutput<TQuery>
>(
queryProcedure: DecorateProcedure<TQuery, any>,
input?: TInput,
params?: UseTRPCQueryOptions<any, TInput, TOutput, TOutput, TError>
) => {
return function WithQuery(
opts: Omit<
Partial<QueryCellOptionsWithEmpty<TQueryValues[TPath]["output"], TError>> &
QueryCellOptionsNoEmpty<TQueryValues[TPath]["output"], TError>,
Partial<QueryCellOptionsWithEmpty<TOutput, TError>> & QueryCellOptionsNoEmpty<TOutput, TError>,
"query"
>
) {
const query = trpc.useQuery(pathAndInput, params);
const query = queryProcedure.useQuery(input, params);
return <QueryCell query={query} {...opts} />;
};
};
+5 -4
View File
@@ -9,7 +9,7 @@ import { ComponentProps, ReactNode } from "react";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { trpc, proxy } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { MetaProvider } from "@calcom/ui/v2/core/Meta";
import usePublicPage from "@lib/hooks/usePublicPage";
@@ -38,8 +38,9 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
* We intend to not cache i18n query
**/
const { i18n, locale } = trpc.useQuery(["viewer.public.i18n"], { trpc: { context: { skipBatch: true } } })
.data ?? {
const { i18n, locale } = trpc.viewer.public.i18n.useQuery(undefined, {
trpc: { context: { skipBatch: true } },
}).data ?? {
locale: "en",
};
@@ -55,7 +56,7 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
};
const AppProviders = (props: AppPropsWithChildren) => {
const session = proxy.public.session.useQuery().data;
const session = trpc.viewer.public.session.useQuery().data;
// No need to have intercom on public pages - Good for Page Performance
const isPublicPage = usePublicPage();
const isThemeSupported =
+1 -1
View File
@@ -1,7 +1,7 @@
import { trpc } from "@calcom/trpc/react";
export function useMeQuery() {
const meQuery = trpc.useQuery(["viewer.me"], {
const meQuery = trpc.viewer.me.useQuery(undefined, {
retry(failureCount) {
return failureCount > 3;
},
@@ -17,9 +17,9 @@ const createRecurringBooking = async (data: ExtendedBookingCreateBody[]) => {
let appsStatus: AppsStatus[] | undefined = undefined;
// Reversing to accumulate results for noEmail instances first, to then lastly, create the
// emailed booking taking into account accumulated results to send app status accurately
for (let key = 0; key < data.length; key++) {
for (let key = data.length - 1; key >= 0; key--) {
const booking = data[key];
if (key === data.length - 1) {
if (key === 0) {
const calcAppsStatus: { [key: string]: AppsStatus } = createdBookings
.flatMap((book) => (book.appsStatus !== undefined ? book.appsStatus : []))
.reduce((prev, curr) => {
@@ -39,6 +39,7 @@ const createRecurringBooking = async (data: ExtendedBookingCreateBody[]) => {
appsStatus,
allRecurringDates,
currentRecurringIndex: key,
noEmail: key !== 0,
});
createdBookings.push(response);
}
-1
View File
@@ -3,7 +3,6 @@ import { RRule } from "rrule";
import dayjs, { Dayjs } from "@calcom/dayjs";
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
import { inferQueryOutput } from "@calcom/trpc/react";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { parseZone } from "./parseZone";
+22 -5
View File
@@ -1,5 +1,6 @@
require("dotenv").config({ path: "../../.env" });
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { withSentryConfig } = require("@sentry/nextjs");
const withTM = require("next-transpile-modules")([
"@calcom/app-store",
@@ -79,6 +80,7 @@ plugins.push(withAxiom);
/** @type {import("next").NextConfig} */
const nextConfig = {
i18n,
productionBrowserSourceMaps: true,
/* We already do type check on GH actions */
typescript: {
ignoreBuildErrors: !!process.env.CI,
@@ -87,10 +89,8 @@ const nextConfig = {
eslint: {
ignoreDuringBuilds: !!process.env.CI,
},
experimental: {
images: {
unoptimized: true,
},
images: {
unoptimized: true,
},
webpack: (config) => {
config.plugins.push(
@@ -231,4 +231,21 @@ const nextConfig = {
},
};
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
const sentryWebpackPluginOptions = {
silent: true, // Suppresses all logs
};
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
nextConfig.sentry = {
hideSourceMaps: true,
// Prevents Sentry from running on this Edge function, where Sentry doesn't work yet (build whould crash the api route).
excludeServerRoutes: [/\/api\/social\/og\/image\/?/],
};
}
// Sentry should be the last thing to export to catch everything right
module.exports = process.env.NEXT_PUBLIC_SENTRY_DSN
? withSentryConfig(moduleExports, sentryWebpackPluginOptions)
: moduleExports;
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.1.4",
"version": "2.2.2",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -19,7 +19,7 @@
"check-changed-files": "ts-node scripts/ts-check-changed-files.ts"
},
"engines": {
"node": ">=14.x <17",
"node": ">=16.x",
"yarn": ">=1.19.0 < 2.0.0"
},
"dependencies": {
@@ -56,6 +56,7 @@
"@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-toggle-group": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
"@sentry/nextjs": "^7.20.0",
"@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9",
+4 -1
View File
@@ -2,6 +2,7 @@
* Typescript class based component for custom-error
* @link https://nextjs.org/docs/advanced-features/custom-error-page
*/
import * as Sentry from "@sentry/nextjs";
import { NextPage, NextPageContext } from "next";
import NextError, { ErrorProps } from "next/error";
import React from "react";
@@ -46,7 +47,9 @@ const CustomError: NextPage<CustomErrorProps> = (props) => {
* Partially adapted from the example in
* https://github.com/vercel/next.js/tree/canary/examples/with-sentry
*/
CustomError.getInitialProps = async ({ res, err, asPath }: AugmentedNextPageContext) => {
CustomError.getInitialProps = async (ctx: AugmentedNextPageContext) => {
const { res, err, asPath } = ctx;
await Sentry.captureUnderscoreErrorException(ctx);
const errorInitialProps = (await NextError.getInitialProps({
res,
err,
+7
View File
@@ -0,0 +1,7 @@
# Before adding new endpoints here
We're deprecating adding new endpoints in favor of creating tRPC procedures.
You can learn about [tRPC procedures in the docs](https://trpc.io/docs/v10/procedures).
You can see our current tRPC procedures in this file `packages/trpc/server/routers/_app.ts`
-85
View File
@@ -1,85 +0,0 @@
import { PrismaClient } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth";
const prisma = new PrismaClient();
type CalendlyEventType = {
name: string;
slug: string;
duration: number;
description_plain: string;
secret: boolean;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const authenticatedUser = await prisma.user.findFirstOrThrow({
where: {
id: session?.user.id,
},
select: {
id: true,
},
});
if (req.method == "POST") {
const userResult = await fetch("https://api.calendly.com/users/me", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + req.body.token,
},
});
if (userResult.status == 200) {
const userData = await userResult.json();
await prisma.user.update({
where: {
id: authenticatedUser.id,
},
data: {
name: userData.resource.name,
},
});
const eventTypesResult = await fetch(
"https://api.calendly.com/event_types?user=" + userData.resource.uri,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + req.body.token,
},
}
);
const eventTypesData = await eventTypesResult.json();
eventTypesData.collection.forEach(async (eventType: CalendlyEventType) => {
await prisma.eventType.create({
data: {
title: eventType.name,
slug: eventType.slug,
length: eventType.duration,
description: eventType.description_plain,
hidden: eventType.secret,
users: {
connect: {
id: authenticatedUser.id,
},
},
userId: authenticatedUser.id,
},
});
});
res.status(201).end();
} else {
res.status(500).end();
}
} else {
res.status(405).end();
}
}
-85
View File
@@ -1,85 +0,0 @@
import { PrismaClient } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth";
const prisma = new PrismaClient();
type SavvyCalEventType = {
name: string;
slug: string;
durations: [number];
description: string;
state: "active";
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const authenticatedUser = await prisma.user.findFirstOrThrow({
where: {
id: session?.user.id,
},
select: {
id: true,
},
});
if (req.method === "POST") {
const userResult = await fetch("https://api.savvycal.com/v1/me", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + req.body.token,
},
});
if (userResult.status === 200) {
const userData = await userResult.json();
await prisma.user.update({
where: {
id: authenticatedUser.id,
},
data: {
name: userData.display_name,
timeZone: userData.time_zone,
weekStart: userData.first_day_of_week === 0 ? "Sunday" : "Monday",
avatar: userData.avatar_url,
},
});
const eventTypesResult = await fetch("https://api.savvycal.com/v1/links?limit=100", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + req.body.token,
},
});
const eventTypesData = await eventTypesResult.json();
eventTypesData.entries.forEach(async (eventType: SavvyCalEventType) => {
await prisma.eventType.create({
data: {
title: eventType.name,
slug: eventType.slug,
length: eventType.durations[0],
description: eventType.description.replace(/<[^>]*>?/gm, ""),
hidden: eventType.state === "active",
users: {
connect: {
id: authenticatedUser.id,
},
},
userId: authenticatedUser.id,
},
});
});
res.status(201).end();
} else {
res.status(500).end();
}
} else {
res.status(405).end();
}
}
-154
View File
@@ -1,154 +0,0 @@
import { BookingStatus, Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!["GET", "DELETE"].includes(req.method || "")) {
return res.status(405).end();
}
// Check that user is authenticated
const session = await getSession({ req });
const userId = session?.user?.id;
if (!userId) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
if (req.method === "GET") {
const credentials = await prisma.credential.findMany({
where: {
userId,
},
select: {
type: true,
},
});
res.status(200).json(credentials);
}
if (req.method == "DELETE") {
const id = req.body.id;
const data: Prisma.UserUpdateInput = {
credentials: {
delete: {
id,
},
},
};
const integration = await prisma.credential.findUnique({
where: {
id,
},
});
/* If the user deletes a zapier integration, we delete all his api keys as well. */
if (integration?.appId === "zapier") {
data.apiKeys = {
deleteMany: {
userId,
appId: "zapier",
},
};
/* We also delete all user's zapier wehbooks */
data.webhooks = {
deleteMany: {
userId,
appId: "zapier",
},
};
}
await prisma.user.update({
where: {
id: userId,
},
data,
});
if (req.body?.action === "cancel" || req.body?.action === "remove") {
try {
const bookingIdsWithPayments = await prisma.booking
.findMany({
where: {
userId: session?.user?.id,
paid: false,
NOT: {
payment: {
every: {
booking: null,
},
},
},
},
select: {
id: true,
},
})
.then((bookings) => bookings.map((booking) => booking.id));
const deletePayments = prisma.payment.deleteMany({
where: {
bookingId: {
in: bookingIdsWithPayments,
},
success: false,
},
});
const updateBookings = prisma.booking.updateMany({
where: {
id: {
in: bookingIdsWithPayments,
},
},
data: {
status: BookingStatus.CANCELLED,
rejectionReason: "Payment provider got removed",
},
});
const bookingReferences = await prisma.booking
.findMany({
where: {
status: BookingStatus.ACCEPTED,
},
select: {
id: true,
},
})
.then((bookings) => bookings.map((booking) => booking.id));
const deleteBookingReferences = prisma.bookingReference.deleteMany({
where: {
bookingId: {
in: bookingReferences,
},
},
});
if (req.body?.action === "cancel") {
await prisma.$transaction([deletePayments, updateBookings, deleteBookingReferences]);
} else {
const updateBookings = prisma.booking.updateMany({
where: {
id: {
in: bookingIdsWithPayments,
},
},
data: {
paid: true,
},
});
await prisma.$transaction([deletePayments, updateBookings]);
}
} catch (e) {
console.error(e);
res.status(500).json({ message: "Integration could not be deleted" });
}
}
res.status(200).json({ message: "Integration deleted successfully" });
}
}
+2 -2
View File
@@ -65,8 +65,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await handler(req, res);
} else {
await defaultIntegrationAddHandler({ user: req.session?.user, ...handler });
redirectUrl = handler.redirectUrl || getInstalledAppPath(handler);
res.json({ url: redirectUrl });
redirectUrl = handler.redirect?.url || getInstalledAppPath(handler);
res.json({ url: redirectUrl, newTab: handler.redirect?.newTab });
}
return res.status(200);
} catch (error) {
@@ -1,48 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import { defaultHandler } from "@calcom/lib/server";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import prisma from "@calcom/prisma";
import { userMetadata as zodUserMetadata } from "@calcom/prisma/zod-utils";
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const { intentUsername } = req.body;
// Check that user is authenticated
try {
const session = await getSession({ req });
const userId = session?.user?.id;
const user = await prisma.user.findFirstOrThrow({
select: {
id: true,
metadata: true,
},
where: { id: userId },
});
const checkPremiumUsernameResult = await checkUsername(intentUsername);
if (userId && user) {
const userMetadata = zodUserMetadata.parse(user.metadata);
await prisma.user.update({
where: {
id: userId,
},
data: {
metadata: {
...userMetadata,
intentUsername,
isIntentPremium: checkPremiumUsernameResult.premium,
},
},
});
}
} catch (error) {
res.status(501).send({ message: "intent-username.save.error" });
}
res.end();
}
export default defaultHandler({
GET: Promise.resolve({ default: getHandler }),
});
-45
View File
@@ -1,45 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { defaultAvatarSrc } from "@lib/profile";
/**
* @deprecated Use TRCP's viewer.me query
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
if (!session) {
res.status(401).json({ message: "Not authenticated" });
return;
}
const user = await prisma.user.findUniqueOrThrow({
where: {
id: session.user.id,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
timeZone: true,
weekStart: true,
startTime: true,
endTime: true,
bufferTime: true,
theme: true,
createdDate: true,
hideBranding: true,
avatar: true,
},
});
user.avatar = user.avatar || defaultAvatarSrc({ email: user.email });
res.status(200).json({
user,
});
}
-55
View File
@@ -1,55 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { getAvailabilityFromSchedule } from "@lib/availability";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const userId = session?.user?.id;
if (!userId) {
res.status(401).json({ message: "Not authenticated" });
return;
}
if (!req.body.schedule || req.body.schedule.length !== 7) {
return res.status(400).json({ message: "Bad Request." });
}
const availability = getAvailabilityFromSchedule(req.body.schedule);
if (req.method === "POST") {
try {
await prisma.user.update({
where: {
id: userId,
},
data: {
availability: {
/* We delete user availabilty */
deleteMany: {
userId: {
equals: userId,
},
},
/* So we can replace it */
createMany: {
data: availability.map((schedule) => ({
days: schedule.days,
startTime: schedule.startTime,
endTime: schedule.endTime,
})),
},
},
},
});
return res.status(200).json({
message: "created",
});
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Unable to create schedule." });
}
}
}
-60
View File
@@ -1,60 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { closeComDeleteTeam } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
if (!session.user?.id) {
console.log("Received session token without a user id.");
return res.status(500).json({ message: "Something went wrong." });
}
if (!req.query.team) {
console.log("Missing team query param.");
return res.status(500).json({ message: "Something went wrong." });
}
const teamId = parseInt(req.query.team as string);
// GET /api/teams/{team}
if (req.method === "GET") {
const team = await getTeamWithMembers(teamId);
return res.status(200).json({ team });
}
// DELETE /api/teams/{team}
if (req.method === "DELETE") {
const membership = await prisma.membership.findFirst({
where: {
userId: session.user.id,
teamId,
},
});
if (!membership || membership.role !== "OWNER") {
console.log(`User ${session.user.id} tried deleting an organization they don't own.`);
return res.status(403).json({ message: "Forbidden." });
}
await prisma.membership.delete({
where: {
userId_teamId: { userId: session.user.id, teamId },
},
});
const deletedTeam = await prisma.team.delete({
where: {
id: teamId,
},
});
// Sync Services: Close.com
closeComDeleteTeam(deletedTeam);
return res.status(204).send(null);
}
}
-132
View File
@@ -1,132 +0,0 @@
import { MembershipRole } from "@prisma/client";
import { randomBytes } from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { sendTeamInviteEmail } from "@calcom/emails";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { BASE_URL } from "@lib/config/constants";
import slugify from "@lib/slugify";
import { getTranslation } from "@server/lib/i18n";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const t = await getTranslation(req.body.language ?? "en", "common");
if (req.method !== "POST") {
return res.status(400).json({ message: "Bad request" });
}
const session = await getSession({ req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
const team = await prisma.team.findFirst({
where: {
id: parseInt(req.query.team as string),
},
});
if (!team) {
return res.status(404).json({ message: "Invalid team" });
}
const reqBody = req.body as {
usernameOrEmail: string;
role: MembershipRole;
sendEmailInvitation: boolean;
};
const { role, sendEmailInvitation } = reqBody;
// liberal email match
const isEmail = (str: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
const usernameOrEmail = isEmail(reqBody.usernameOrEmail)
? reqBody.usernameOrEmail.toLowerCase()
: slugify(reqBody.usernameOrEmail);
const invitee = await prisma.user.findFirst({
where: {
OR: [{ username: usernameOrEmail }, { email: usernameOrEmail }],
},
});
if (!invitee) {
const email = isEmail(usernameOrEmail) ? usernameOrEmail : undefined;
if (!email) {
return res.status(400).json({
message: `Invite failed because there is no corresponding user for ${usernameOrEmail}`,
});
}
await prisma.user.create({
data: {
email,
teams: {
create: {
team: {
connect: {
id: parseInt(req.query.team as string),
},
},
role,
},
},
},
});
const token: string = randomBytes(32).toString("hex");
await prisma.verificationToken.create({
data: {
identifier: usernameOrEmail,
token,
expires: new Date(new Date().setHours(168)), // +1 week
},
});
if (session?.user?.name && team?.name) {
await sendTeamInviteEmail({
language: t,
from: session.user.name,
to: usernameOrEmail,
teamName: team.name,
joinLink: `${BASE_URL}/auth/signup?token=${token}&callbackUrl=/settings/teams}`,
});
}
return res.status(201).json({});
}
// create provisional membership
try {
await prisma.membership.create({
data: {
teamId: parseInt(req.query.team as string),
userId: invitee.id,
role,
},
});
} catch (err: any) {
if (err.code === "P2002") {
// unique constraint violation
return res.status(409).json({
message: "This user is a member of this team / has a pending invitation.",
});
} else {
throw err; // rethrow
}
}
// inform user of membership by email
if (sendEmailInvitation && session?.user?.name && team?.name) {
await sendTeamInviteEmail({
language: t,
from: session.user.name,
to: usernameOrEmail,
teamName: team.name,
joinLink: BASE_URL + "/settings/teams",
});
}
res.status(201).json({});
}
@@ -1,86 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { closeComDeleteTeamMembership } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
if (!session) {
res.status(401).json({ message: "Not authenticated" });
return;
}
const isTeamOwner = !!(await prisma.membership.findFirst({
where: {
userId: session.user?.id,
teamId: parseInt(req.query.team as string),
role: "OWNER",
},
}));
if (!isTeamOwner) {
res.status(403).json({ message: "You are not authorized to manage this team" });
return;
}
// List members
if (req.method === "GET") {
const memberships = await prisma.membership.findMany({
where: {
teamId: parseInt(req.query.team as string),
},
});
let members = await prisma.user.findMany({
where: {
id: {
in: memberships.map((membership) => membership.userId),
},
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
timeZone: true,
},
});
members = members.map((member) => {
const membership = memberships.find((membership) => member.id === membership.userId);
return {
...member,
role: membership?.accepted ? membership?.role : "INVITEE",
};
});
return res.status(200).json({ members: members });
}
// Cancel a membership (invite)
if (req.method === "DELETE") {
const user = await prisma.user.findFirst({
where: {
id: req.body.userId,
},
});
await prisma.membership.delete({
where: {
userId_teamId: { userId: req.body.userId, teamId: parseInt(req.query.team as string) },
},
});
// Sync Services: Close.com
closeComDeleteTeamMembership(user);
return res.status(204).send(null);
}
// Promote or demote a member of the team
res.status(200).json({});
}
@@ -1,71 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
// @deprecated - USE TRPC
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
if (!session?.user?.id) {
return res.status(401).json({ message: "Not authenticated" });
}
const isTeamOwner = !!(await prisma.membership.findFirst({
where: {
userId: session.user.id,
teamId: parseInt(req.query.team as string),
role: "OWNER",
},
}));
if (!isTeamOwner) {
res.status(403).json({ message: "You are not authorized to manage this team" });
return;
}
// PATCH /api/teams/profile/{team}
if (req.method === "PATCH") {
const team = await prisma.team.findFirst({
where: {
id: parseInt(req.query.team as string),
},
});
if (!team) {
return res.status(404).json({ message: "Invalid team" });
}
const username = req.body.username;
const userConflict = await prisma.team.findMany({
where: {
slug: username,
},
});
const teamId = Number(req.query.team);
if (userConflict.some((team) => team.id !== teamId)) {
return res.status(409).json({ message: "Team username already taken" });
}
const name = req.body.name;
const slug = req.body.username;
const bio = req.body.description;
const logo = req.body.logo;
const hideBranding = req.body.hideBranding;
await prisma.team.update({
where: {
id: team.id,
},
data: {
name,
slug,
logo,
bio,
hideBranding,
},
});
return res.status(200).json({ message: "Team updated successfully" });
}
}
-54
View File
@@ -1,54 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getStripeCustomerId } from "@calcom/app-store/stripepayment/lib/customer";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { WEBSITE_URL } from "@lib/config/constants";
import { HttpError as HttpCode } from "@lib/core/http/error";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
if (!session?.user?.id) {
return res.status(401).json({ message: "Not authenticated" });
}
if (!["GET", "POST"].includes(req.method || "")) {
throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
}
const user = await prisma.user.findUniqueOrThrow({
where: {
id: session.user.id,
},
select: {
email: true,
metadata: true,
},
});
const stripeCustomerId = await getStripeCustomerId(user);
try {
const response = await fetch(`${WEBSITE_URL}/api/upgrade`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
stripeCustomerId,
email: user.email,
fromApp: true,
}),
});
const data = await response.json();
if (!data.url) throw new HttpCode({ statusCode: 401, message: data.message });
res.redirect(303, data.url);
} catch (error) {
console.error(`error`, error);
res.redirect(303, req.headers.origin || "/");
}
}
-86
View File
@@ -1,86 +0,0 @@
import { pick } from "lodash";
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
if (!session?.user.id) {
return res.status(401).json({ message: "Not authenticated" });
}
const querySchema = z.object({
id: z.string().transform((val) => parseInt(val)),
});
const parsedQuery = querySchema.safeParse(req.query);
const userId = parsedQuery.success ? parsedQuery.data.id : null;
if (!userId) {
return res.status(400).json({ message: "No user id provided" });
}
const authenticatedUser = await prisma.user.findFirstOrThrow({
where: {
id: session.user.id,
},
select: {
id: true,
},
});
if (userId !== authenticatedUser.id) {
return res.status(401).json({ message: "Unauthorized" });
}
if (req.method === "GET") {
return res.status(405).json({ message: "Method Not Allowed" });
}
if (req.method === "PATCH") {
const updatedUser = await prisma.user.update({
where: {
id: authenticatedUser.id,
},
data: {
...pick(req.body.data, [
"username",
"name",
"avatar",
"timeZone",
"timeFormat",
"weekStart",
"hideBranding",
"theme",
"completedOnboarding",
]),
bio: req.body.description ?? req.body.data?.bio,
},
select: {
id: true,
username: true,
name: true,
email: true,
emailVerified: true,
bio: true,
avatar: true,
timeZone: true,
timeFormat: true,
weekStart: true,
startTime: true,
endTime: true,
bufferTime: true,
hideBranding: true,
theme: true,
createdDate: true,
plan: true,
completedOnboarding: true,
},
});
return res.status(200).json({ message: "User Updated", data: updatedUser });
}
}
-50
View File
@@ -1,50 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
import { deleteWebUser as syncServicesDeleteWebUser } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
if (!session?.user.id) {
return res.status(401).json({ message: "Not authenticated" });
}
if (req.method !== "DELETE") {
return res.status(405).json({ message: "Method Not Allowed" });
}
if (req.method === "DELETE") {
// Get user
const user = await prisma.user.findUniqueOrThrow({
where: {
id: session.user?.id,
},
select: {
id: true,
email: true,
metadata: true,
username: true,
createdDate: true,
name: true,
plan: true,
},
});
// Delete from stripe
await deleteStripeCustomer(user).catch(console.warn);
// Delete from Cal
await prisma.user.delete({
where: {
id: session?.user.id,
},
});
// Sync Services
syncServicesDeleteWebUser(user);
return res.status(204).end();
}
}
-63
View File
@@ -1,63 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
if (!session || !session.user?.id) {
return res.status(401).json({ message: "Not authenticated" });
}
if (req.method === "GET") {
const memberships = await prisma.membership.findMany({
where: {
userId: session.user.id,
},
});
const teams = await prisma.team.findMany({
where: {
id: {
in: memberships.map((membership) => membership.teamId),
},
},
});
return res.status(200).json({
membership: memberships.map((membership) => ({
role: membership.accepted ? membership.role : "INVITEE",
...teams.find((team) => team.id === membership.teamId),
})),
});
}
if (!req.body.teamId) {
return res.status(400).json({ message: "Bad request" });
}
// Leave team or decline membership invite of current user
if (req.method === "DELETE") {
await prisma.membership.delete({
where: {
userId_teamId: { userId: session.user.id, teamId: req.body.teamId },
},
});
return res.status(204).send(null);
}
// Accept team invitation
if (req.method === "PATCH") {
await prisma.membership.update({
where: {
userId_teamId: { userId: session.user.id, teamId: req.body.teamId },
},
data: {
accepted: true,
},
});
return res.status(204).send(null);
}
}
-50
View File
@@ -1,50 +0,0 @@
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { pick } from "lodash";
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { resizeBase64Image } from "@server/lib/resizeBase64Image";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
if (!session) {
res.status(401).json({ message: "Not authenticated" });
return;
}
try {
const avatar = req.body.avatar ? await resizeBase64Image(req.body.avatar) : undefined;
await prisma.user.update({
where: {
id: session.user.id,
},
data: {
...pick(req.body, [
"username",
"name",
"timeZone",
"weekStart",
"hideBranding",
"theme",
"completedOnboarding",
"locale",
]),
avatar,
bio: req.body.description,
},
});
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e.code === "P2002") {
return res.status(409).json({ message: "Username already taken" });
}
}
throw e;
}
return res.status(200).json({ message: "Profile updated successfully" });
}
-28
View File
@@ -1,28 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { ensureSession } from "@calcom/lib/auth";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
// Only logged in users can opt-in/out
await ensureSession({ req });
// If has the cookie, Opt-out of V2
if ("calcom-v2-early-access" in req.cookies && req.cookies["calcom-v2-early-access"] === "1") {
res.setHeader("Set-Cookie", `calcom-v2-early-access=0; Max-Age=0; Path=/`);
} else {
/* Opt-int to V2 */
res.setHeader("Set-Cookie", "calcom-v2-early-access=1; Path=/");
}
let redirectUrl = "/";
// We take you back where you came from if possible
if (typeof req.headers["referer"] === "string") redirectUrl = req.headers["referer"];
res.redirect(redirectUrl);
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});
+4 -4
View File
@@ -5,7 +5,7 @@ import { AppSettings } from "@calcom/app-store/_components/AppSettings";
import { InstallAppButton } from "@calcom/app-store/components";
import { InstalledAppVariants } from "@calcom/app-store/utils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { App } from "@calcom/types/App";
import { AppGetServerSidePropsContext } from "@calcom/types/AppGetServerSideProps";
import { Icon } from "@calcom/ui/Icon";
@@ -36,7 +36,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
const utils = trpc.useContext();
const handleOpenChange = () => {
utils.invalidateQueries(["viewer.integrations"]);
utils.viewer.integrations.invalidate();
};
if (credentialId) {
@@ -96,7 +96,7 @@ interface IntegrationsContainerProps {
interface IntegrationsListProps {
variant?: IntegrationsContainerProps["variant"];
data: inferQueryOutput<"viewer.integrations">;
data: RouterOutputs["viewer"]["integrations"];
}
const IntegrationsList = ({ data }: IntegrationsListProps) => {
@@ -134,7 +134,7 @@ const IntegrationsList = ({ data }: IntegrationsListProps) => {
const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps): JSX.Element => {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.integrations", { variant, exclude, onlyInstalled: true }]);
const query = trpc.viewer.integrations.useQuery({ variant, exclude, onlyInstalled: true });
const emptyIcon = {
calendar: Icon.FiCalendar,
conferencing: Icon.FiVideo,
+1 -1
View File
@@ -90,7 +90,7 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
return (
<FormProvider {...formMethods}>
<form id="setup-step-1" name="setup-step-1" className="space-y-4" onSubmit={onSubmit}>
<form id="wizard-step-1" name="wizard-step-1" className="space-y-4" onSubmit={onSubmit}>
<div>
<Controller
name="username"
+4 -7
View File
@@ -57,13 +57,10 @@ export default function Verify() {
const router = useRouter();
const { t, sessionId, stripeCustomerId } = querySchema.parse(router.query);
const [secondsLeft, setSecondsLeft] = useState(30);
const { data } = trpc.useQuery([
"viewer.public.stripeCheckoutSession",
{
stripeCustomerId,
checkoutSessionId: sessionId,
},
]);
const { data } = trpc.viewer.public.stripeCheckoutSession.useQuery({
stripeCustomerId,
checkoutSessionId: sessionId,
});
useSendFirstVerificationLogin({ email: data?.customer?.email, username: data?.customer?.username });
// @note: check for t=timestamp and apply disabled state and secondsLeft accordingly
// to avoid refresh to skip waiting 30 seconds to re-send email
+6 -6
View File
@@ -41,7 +41,7 @@ export default function Availability({ schedule }: { schedule: number }) {
const utils = trpc.useContext();
const me = useMeQuery();
const { timeFormat } = me.data || { timeFormat: null };
const { data, isLoading } = trpc.useQuery(["viewer.availability.schedule", { scheduleId: schedule }]);
const { data, isLoading } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId: schedule });
const form = useForm<AvailabilityFormValues>();
const { control, reset } = form;
@@ -57,18 +57,18 @@ export default function Availability({ schedule }: { schedule: number }) {
}
}, [data, isLoading, reset]);
const updateMutation = trpc.useMutation("viewer.availability.schedule.update", {
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
if (prevDefaultId && currentDefaultId) {
// check weather the default schedule has been changed by comparing previous default schedule id and current default schedule id.
if (prevDefaultId !== currentDefaultId) {
// if not equal, invalidate previous default schedule id and refetch previous default schedule id.
utils.invalidateQueries(["viewer.availability.schedule", { scheduleId: prevDefaultId }]);
utils.refetchQueries(["viewer.availability.schedule", { scheduleId: prevDefaultId }]);
utils.viewer.availability.schedule.get.invalidate({ scheduleId: prevDefaultId });
utils.viewer.availability.schedule.get.refetch({ scheduleId: prevDefaultId });
}
}
utils.setQueryData(["viewer.availability.schedule", { scheduleId: data.schedule.id }], data);
utils.invalidateQueries(["viewer.availability.list"]);
utils.viewer.availability.schedule.get.setData({ scheduleId: data.schedule.id }, data);
utils.viewer.availability.list.invalidate();
showToast(
t("availability_updated_successfully", {
scheduleName: data.schedule.name,
+10 -10
View File
@@ -2,7 +2,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui/Icon";
import Shell from "@calcom/ui/Shell";
import { EmptyScreen, showToast } from "@calcom/ui/v2";
@@ -12,19 +12,19 @@ import { HttpError } from "@lib/core/http/error";
import SkeletonLoader from "@components/availability/SkeletonLoader";
export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) {
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
const { t } = useLocale();
const utils = trpc.useContext();
const meQuery = trpc.useQuery(["viewer.me"]);
const meQuery = trpc.viewer.me.useQuery();
const deleteMutation = trpc.useMutation("viewer.availability.schedule.delete", {
const deleteMutation = trpc.viewer.availability.schedule.delete.useMutation({
onMutate: async ({ scheduleId }) => {
await utils.cancelQuery(["viewer.availability.list"]);
const previousValue = utils.getQueryData(["viewer.availability.list"]);
await utils.viewer.availability.list.cancel();
const previousValue = utils.viewer.availability.list.getData();
if (previousValue) {
const filteredValue = previousValue.schedules.filter(({ id }) => id !== scheduleId);
utils.setQueryData(["viewer.availability.list"], { ...previousValue, schedules: filteredValue });
utils.viewer.availability.list.setData(undefined, { ...previousValue, schedules: filteredValue });
}
return { previousValue };
@@ -32,7 +32,7 @@ export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availab
onError: (err, variables, context) => {
if (context?.previousValue) {
utils.setQueryData(["viewer.availability.list"], context.previousValue);
utils.viewer.availability.list.setData(undefined, context.previousValue);
}
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
@@ -40,7 +40,7 @@ export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availab
}
},
onSettled: () => {
utils.invalidateQueries(["viewer.availability.list"]);
utils.viewer.availability.list.invalidate();
},
onSuccess: () => {
showToast(t("schedule_deleted_successfully"), "success");
@@ -83,7 +83,7 @@ export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availab
);
}
const WithQuery = withQuery(["viewer.availability.list"]);
const WithQuery = withQuery(trpc.viewer.availability.list);
export default function AvailabilityPage() {
const { t } = useLocale();
+10 -13
View File
@@ -2,11 +2,11 @@ import { useState } from "react";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import Shell from "@calcom/ui/Shell";
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
type User = inferQueryOutput<"viewer.me">;
type User = RouterOutputs["viewer"]["me"];
export interface IBusySlot {
start: string | Date;
@@ -19,16 +19,13 @@ const AvailabilityView = ({ user }: { user: User }) => {
const { t } = useLocale();
const [selectedDate, setSelectedDate] = useState(dayjs());
const { data, isLoading } = trpc.useQuery(
[
"viewer.availability.user",
{
username: user.username!,
dateFrom: selectedDate.startOf("day").utc().format(),
dateTo: selectedDate.endOf("day").utc().format(),
withSource: true,
},
],
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
{
username: user.username!,
dateFrom: selectedDate.startOf("day").utc().format(),
dateTo: selectedDate.endOf("day").utc().format(),
withSource: true,
},
{
enabled: !!user.username,
}
@@ -99,7 +96,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
};
export default function Troubleshoot() {
const { data, isLoading } = trpc.useQuery(["viewer.me"]);
const { data, isLoading } = trpc.viewer.me.useQuery();
const { t } = useLocale();
return (
<div>

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