Compare commits

...
206 Commits
Author SHA1 Message Date
Keith Williams 75d5576e17 v3.8.2
release.yml / release (push) Canceled after 0s
2024-02-20 21:41:13 -03:00
Keith Williams 0a6dde4fcc chore: Add extra logging for no available users error (#13791)
* chore: Add extra logging for no available users error

* Changed warn to info
2024-02-20 23:51:31 +00:00
Omar López 73b5839284 fix: og images avatars (#13790)
* fix og images avatars

* Update OgImages.tsx

* More fixes

* Update getPublicEvent.ts

* Update HeadSeo.tsx
2024-02-20 23:04:59 +00:00
Peer Richelsen 1a42b94b13 feat: added linears google calendar app (#13789) 2024-02-20 20:08:08 +00:00
Erik acb910f680 v3.8.1
release.yml / release (push) Canceled after 0s
2024-02-20 15:20:09 -03:00
Crowdin Bot 38e857f9a9 New Crowdin translations by Github Action 2024-02-20 15:30:02 +00:00
Crowdin Bot b8149dee36 New Crowdin translations by Github Action 2024-02-20 15:25:22 +00:00
Crowdin Bot f583ef40f2 New Crowdin translations by Github Action 2024-02-20 15:19:54 +00:00
Keith WilliamsandOmar López 71574488c5 fix: No available users found (#13738)
* fix: No available users found

* Update getUserAvailability.ts

* Update getUserAvailability.ts

* Update handleNewBooking.ts

* Reusing the parsed dates throughout the full stack

* Fixed typo

* Added unit tests for stringToDayjs

* Fixing types

* Reverted the getSchedule utc change since that endpoint takes in UTC formatted dates but with a given timezone

* chore: Add test replicating 'no available users' error

* Test is passing

* Name clarifications

* Test clean up

* Update packages/features/bookings/lib/handleNewBooking.ts

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

* Update handleNewBooking.ts

* Fixed issue where date overrides ending at midnight don't allow bookings

* Removed comment

---------

Co-authored-by: Omar López <zomars@me.com>
2024-02-20 12:15:05 -03:00
Omar López 008e0e43a2 chore: Drop deprecated password column (#13698) 2024-02-20 14:52:51 +00:00
Crowdin Bot da22eda662 New Crowdin translations by Github Action 2024-02-20 10:27:49 +00:00
sean-brydonandUdit Takkar f94d696885 fix: text-overflow (#13775)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-20 10:23:49 +00:00
Crowdin Bot 9e84eb1b2c New Crowdin translations by Github Action 2024-02-19 21:01:08 +00:00
zomars 4c549811a1 chore: Add explainer to RawHtml 2024-02-19 13:57:01 -07:00
Hariom Balhara 05502f0cfb fix: Old username link on org user's profile. (#13753) 2024-02-19 19:09:11 +00:00
mokkin e436b8e06b fix: correct German translation (#13742) 2024-02-19 19:07:18 +00:00
Crowdin Bot 8146cf8c93 New Crowdin translations by Github Action 2024-02-19 16:55:08 +00:00
Henrik KleeandKeith Williams c05f595ef0 reimplement #12681 (#13639)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-19 13:51:09 -03:00
Crowdin Bot 0a795f5363 New Crowdin translations by Github Action 2024-02-16 17:53:19 +00:00
5b50a469b2 fix: double bookings on seated event (#13406)
* fix: double bookings on seated event

* test: correcting handleSeats.test.ts

fix: seat rescheduling

* Add test for booking w/o bookingUid in req

* Add back `bookingUid`

* add 'reqBookingUid' parameter

* Add `reqBookingUid`

* Fix

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2024-02-16 12:48:57 -05:00
Alex van Andel c08365f09c feat: Implement orgs/teams logoUrls (#13699)
* feat: Implement orgs/teams logoUrls

* Use getAvatarUrl in team/[slug]

* Extract uploadLogo to standalone file for reuse

* Allow removing the team/organisation logo

* fix: API build fail due to server-only, removed
2024-02-16 10:35:53 -03:00
Shyam Raghuwanshi 477e102388 feat: Split-large-code-file-functions (#13592)
* Split large code file functions

* move the files from root into lib
2024-02-16 10:32:00 -03:00
Udit Takkar e1b10b4ec5 fix: update email flaky test (#13727) 2024-02-16 10:29:08 -03:00
Hariom BalharaandUdit Takkar ff6db3c572 fix: team invite signup not working (#13717)
* Fix team invite signup not working

* Add unit test

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-16 12:50:07 +00:00
Crowdin Bot 4fa3fdef97 New Crowdin translations by Github Action 2024-02-16 12:35:41 +00:00
Hariom Balhara f29f6632e8 fix: Managed children event listing for organization members (#13722)
* Fetch event-types by userId for childEvents and filter out those events that dont belong to the user

* Add test
2024-02-16 09:32:05 -03:00
Crowdin Bot 36747988e6 New Crowdin translations by Github Action 2024-02-16 11:41:38 +00:00
Morgan 3dd312b88b chore: booker web wrapper (#13676)
* chore: booker web wrapper

* fixup! chore: booker web wrapper

* fixup! fixup! chore: booker web wrapper

* fixup! fixup! fixup! chore: booker web wrapper

* fixup! fixup! fixup! fixup! chore: booker web wrapper

* fix: CreateANewOrganizationForm verify dialog

* fix: EventBooker not rerendering when it should

* fix: init booker store properly

* fixup! fix: init booker store properly

* fix: useMemo EventBooker

* fix: remove unnecessary file
2024-02-16 11:38:12 +00:00
Crowdin Bot 3c9e9329b0 New Crowdin translations by Github Action 2024-02-16 10:58:45 +00:00
Udit Takkar 8617f43a8e chore: add toast and tooltip (#13724)
* chore: add toast and tooltop

* chore: update text
2024-02-16 10:54:31 +00:00
Crowdin Bot 5f0eb652b0 New Crowdin translations by Github Action 2024-02-16 08:35:42 +00:00
JackandUdit Takkar 26d7301885 fix: [CAL-3141] (+1) badge not aligned (#13718)
* Fixes [CAL-3141] (+1) badge not aligned

* fix: centering

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-02-16 08:32:46 +00:00
Crowdin Bot ffc4d22ff1 New Crowdin translations by Github Action 2024-02-15 22:47:32 +00:00
ee286a600f refactor: Only send dirty fields for event type update call (#13292)
* init

* typefix maybe

* fix type

* fix e2e

* Extract delete dialog to its own function to optimise rerender

* wip

* Delete isEqual

* adds eslint tsignore comment

* only update changed fields

* fix type err

* --WIP

* fix type err isLoading—>isPending

* isLoading—>isPending

* post conflict resoution fix

* shouldDirty minimumBookingNotice

* adds shouldDirty to setValues

* reset form to ensure updated default values

* shouldDirty changes and fixed deep check of dirty

* --WIP

* should dirty recurring

* shouldDirty team tab

* multiple-fixes

* adding console log to test something in e2e

* shouldDirty apps tab

* fixes managed event type children association

* fixed managed events update

* dirty managed fix

* fixes some onChange versus setValue and periodCountCalendarDays

* Seeing different behaviour explicitly unwrapping formState

* only mutate if at least 1 field dirty

* fix type error

* bunch of fixes

* --WIP

* update only dirtyFields for Managed Events existing children

* Fixes update old managed event type tests --WIP

* Fix unit tests

A bunch of unit test fixes which started failing
due to update fields only requiring actual updated
fields. some other small fixes also included

* type fix

* remove unnecessary comment

* Adds dirtyField to instant event

* fix: add shouldDirty

* fixes availability and team tabs, and disable save button unless dirty

* fix for hosts and bookerLayout

* add shouldDirty when resetting hosts after changing schedulingType

* add missing shouldDirty to useEventTypeDestinationCalendarEmail

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-02-15 22:44:20 +00:00
Udit Takkar 72c493ee21 chore: add instruction for #12821 (#13713)
release.yml / release (push) Canceled after 0s
2024-02-16 01:22:12 +05:30
Alex van Andel 5bd0428d18 v3.8.0
release.yml / release (push) Canceled after 0s
2024-02-15 15:54:47 +00:00
c7089b9db6 feat: store event-type filters to localStorage (#13407)
* Updated useTypedQuery to store values to localstorage

* removed the redundant function params

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-02-15 10:03:42 -05:00
Morgan 81ad00a816 fix: safely delete userPassword (#13705)
* fix: safely delete userPassword

* fixup! fix: safely delete userPassword

* fixup! Merge branch 'main' into fix-safely-delete-userpassword
2024-02-15 13:19:31 +00:00
Udit TakkarandMorgan b5730da56d fix: password reset when provider is changed (#13706)
* fix: password reset when provider is changed

* chore: remove log

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-02-15 13:19:11 +00:00
Zeeshan BhatiandJoe Au-Yeung 9455d14ad5 feat: Add Google Profile Photo when connecting Google Calendar (#13627)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-02-15 11:51:24 +00:00
LAVIN RAJ MOHANandUdit Takkar a056160404 fix(fix/null-safety):#13673 (#13674)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-15 07:17:19 +00:00
Omar López 6d53cd84f8 v3.7.19
release.yml / release (push) Canceled after 0s
2024-02-14 14:19:56 -07:00
Omar López 30cb8f745d revert: Revert UserPassword (#13680) (#13685)
* Revert "revert: UserPassword (#13680)"

This reverts commit 0a68c6d048.

* fix: avoid updates for possible missing passwords
2024-02-14 16:53:27 +00:00
bafa3c97ac fix: Organizations Admin improvements (#13656)
* Admin improvements

* chore: improve spacing

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-02-14 10:34:45 -03:00
Alex van Andel bb48ccbad9 v3.7.18
release.yml / release (push) Canceled after 0s
2024-02-14 12:42:33 +00:00
Alex van Andel 0a68c6d048 revert: UserPassword (#13680) 2024-02-14 12:36:29 +00:00
Peer RichelsenandHariom bb9f8e5d38 fix: dont link to org url inside embeds (#13668)
* dont link to org url inside embeds

* Add comment

* Fix type issue

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-02-14 11:31:24 +05:30
Carina WollendorferandCarinaWolli c99bb190f1 fix: route always selects 'custom' after saving an event redirect (#13665)
* fix to not always switch to custom on fresh load

* type fix

* type fix

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-02-13 22:08:03 +00:00
Omar López f0d2a45fdf v3.7.17
release.yml / release (push) Canceled after 0s
2024-02-13 14:58:51 -07:00
zomars e5f9315ee6 fix: faulty redirect 2024-02-13 12:45:21 -07:00
zomars f7782b2080 fix: hotfix failed migration 2024-02-13 12:16:38 -07:00
Omar López e1f2dbab84 fix: amend user password migration (#13662) 2024-02-13 18:40:50 +00:00
Adam Azzam c2edbbcea7 Update SettingsToggle.tsx (#13660) 2024-02-13 22:54:27 +05:30
c7e78cc181 fix: AI build (#13657)
* fix: AI build

* Updated zod to 3.22.4

* yarn.lock

* Updated yarn.lock to prevent next/tw downgrade

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-02-13 16:10:08 +00:00
7ba1557d1d fix: use destination calendar email (#12821)
* fix: use destination calendar email

* fix: success page

* feat: add primary email in destination calendar

* fix: set destination bug

* chore: fix tests

* test: add test for different email

* wip: save progress

* feat: add switch and email in booking

* fix: use userPrimaryEmail

* chore: fix type errr

* chore: update buileder

* fix: type err in advanced tab

* fix: unit tests

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

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

* fix: cancel, request reschedule, paymeny

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-02-13 10:05:00 -05:00
Suyash SrivastavaandPeer Richelsen d5b45b757a chore: Removed Skiff from the App Store (#13650)
* rule for Prisma that includes using only "true" and not a field selector

* Removed Skiff from the app store

* Apply suggestions from code review

* Delete packages/eslint-plugin/src/rules/disallow-prisma-include-only-true-not-field-selector.ts

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
2024-02-13 11:13:12 +00:00
bb3b262bd7 fix: Add location to ics file (#13646)
* Add location to ics file

* Add tests

* fix: type err

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-02-13 16:35:04 +05:30
d4f41e803a fix: Collective Events: Duration parameter in link not working (#13523)
* set the selectedDuration in store to duration passed in query

* Pass durationConfig prop to Booker component in /team/[slig]/[type] pages

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-02-13 12:23:27 +05:30
74eafa59e1 fix: loading animation of update buttons (#13131)
* fixed loading animation of update buttons

* Update BookerLayoutSelector.tsx

* fix: update button animation in settings appearance page

* Update appearance.tsx

* Update appearance.tsx

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-13 06:32:21 +00:00
Udit Takkarandzomars e6cd016cad fix: usage of CAL_URL (#13326)
Co-authored-by: zomars <zomars@me.com>
2024-02-13 06:03:45 +00:00
Omar López 7b0d5959cb refactor: migrates user password to it's own table (#13628) 2024-02-12 14:13:21 -07:00
Alex van Andel 7702ded85e v3.7.16
release.yml / release (push) Canceled after 0s
2024-02-12 18:31:59 +00:00
Alex van Andel 5acb3e8f97 fix: Missing date ranges when specifying a specific slot time (#13645) 2024-02-12 18:25:49 +00:00
sean-brydon 41046aaf27 fix: use org logo for sub teams (#13632)
* fix: use org logo for sub teams

* chore: remove unneeded type
2024-02-12 12:59:25 -03:00
Hariom Balhara 7cffb30daa fix: Errors due to namespacing of embed (#13637) 2024-02-12 07:46:08 -07:00
Alex van Andel 26bacb6c8e fix: Regenerated lock file 2024-02-12 13:38:13 +00:00
Crowdin Bot 01b5c1f11e New Crowdin translations by Github Action 2024-02-11 14:32:05 +00:00
Anik Dhabal Babu f5ba04599b fix: unable to setup localy (#13634) 2024-02-11 14:28:40 +00:00
Brace Sproul 994e602c8d refactor: Bump LangChain to 0.1 & update other LangChain imports (#13622) 2024-02-10 19:30:42 +00:00
Carina WollendorferandCarinaWolli a776c72d48 fix: improvements for variables in routing-from routes (#13624)
* add variable description

* type fixes

* type fixes

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-02-10 19:28:59 +00:00
sean-brydon 6adb5b413d fix: troubleshooter scroll (#13631) 2024-02-10 19:26:21 +00:00
alannnc b183a5779a fix: oncancel modal action week layout (#13625) 2024-02-09 23:02:05 +00:00
alannnc 16c1cf7b23 fix ooo section to use settings layout (#13623) 2024-02-09 19:57:08 -03:00
Alex van Andel cf4ee142de v3.7.15
release.yml / release (push) Canceled after 0s
2024-02-09 17:46:11 +00:00
Keith Williams a5d99bbec9 chore: Remove platform from label ignore list (#13618) 2024-02-09 12:42:56 -03:00
Crowdin Bot 5d0468a986 New Crowdin translations by Github Action 2024-02-09 14:11:25 +00:00
Hariom Balhara 7fef9db4ce fix: Reduce payload even further. (#13599)
* Revert "Revert event-type profile based querying (#13588)"

This reverts commit c0c40185fc.

* Add huge event-types seeder

* Fix missing where causing wrong event-types for user
2024-02-09 09:07:26 -05:00
Carina WollendorferandCarinaWolli 296a367955 feat: round-robin priority ranking (#13566)
* improve design of assignment tab

* design fix

* add prioity dialog

* implement UI

* add backend to save priorities

* add priorty to rr algorithm + tests

* fix defaulValue for select + added translations

* add toggle for round robin fixed hosts

* don't show priorities for fixed hosts

* type fixes

* code clean up and type fixes

* clean up getValues and setValue

* code clean up

* fix tests

* code clean up

* fix undefined check

* allow null for priority

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-02-09 12:12:06 +00:00
Crowdin Bot 1ff2d51ee5 New Crowdin translations by Github Action 2024-02-09 10:17:14 +00:00
Morgan 092cb0f691 fix: organizer_default_conferencing_app translatable key in app location (#13614) 2024-02-09 10:13:30 +00:00
zomars 0c74a98721 upgrades copycat 2024-02-08 14:11:24 -07:00
Omar LópezandKeith Williams 2082a4f4ed chore: upgrades boxyhq jackson (#13477)
* chore: upgrades boxyhq jackson

* Update jackson.ts

reduces bundle size

* Update yarn.lock

* Update apps/web/next.config.js

* Upgrades again

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-08 17:51:29 -03:00
Anant JainandUdit Takkar f06ef84f61 fix: Fixes teams UI load issue (#13593)
* fixes teams uiload issue

* revert: yarn.lock

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-02-08 18:58:21 +00:00
Udit Takkar 1080d7f88f fix: update valid_for_secs (#13604) 2024-02-08 18:46:02 +00:00
4ebd52c9af test: Create unit tests for the questions (teste2e-multiSelectQuestion) (#11569)
* Remove unnecessary changes

* add changes

* Requested changes

* Requested changes

* FIx failing tests

* FIx failing tests

* Update regularBookings.ts

* FIx failing tests

* Refactor

* add unit tests for all questions

---------

Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-02-08 15:00:19 -03:00
Morgan e0ac95578c chore: refactor handle new reccuring booking (#13597)
* chore: refactor handle new reccuring booking

* fixup! chore: refactor handle new reccuring booking

* fixup! fixup! chore: refactor handle new reccuring booking
2024-02-08 16:59:49 +00:00
Hariom BalharaandJoe Au-Yeung 473012f2c0 fix: organization user shouldn't be asked for payment for a premium username (#13535)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-02-08 16:46:21 +00:00
Hariom BalharaandJoe Au-Yeung 1293588ac4 test: Bookings: Add more automated tests for organization (#13576)
* Avoid selecting unused props

* Add automated tests

* Add existing user invite and booking

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-02-08 16:20:00 +00:00
Hariom Balhara c0c40185fc Revert event-type profile based querying (#13588) 2024-02-08 09:40:17 -03:00
3348e7be2f fix: prevent duplicate calendar account linking (#13310)
* fix: prevent signing up multiple times from same account

* revert: lark calendar changes

* credential clean up if duplicate

* fix code duplication and check before credential creation

* feat: useRouterQuery allow unset queryParam

* feat: showToast on account duplication attempt

* Small tweak to copy

* Updated other calendars not to use checkDuplicateCalendar

* Add account already linked to apps/installed/calendar

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-08 10:52:32 +00:00
97dcb2a2a9 fix: add missing title (#13307)
* add missing title

* add

* update

* update

* update

* fix lint

* Update AppsLayout.tsx

* update

* Update bookings-listing-view.tsx

* update

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-07 17:49:46 -03:00
Udit Takkar 5dad78f8fe fix: 2FA OTP flaky test (#13583)
* fix: 2FA OTP flaky test

* chore: change timeout
2024-02-07 19:02:23 +00:00
zomars 88cc83a0aa Update transform.ts 2024-02-07 10:57:06 -07:00
zomars 9dddf5e525 fix: docker's toomanyrequests error 2024-02-07 10:33:47 -07:00
Lauris Skraucis 9df9f9bb6b refactor: trpc get schedule handler (#13586) 2024-02-07 09:43:19 -07:00
LaneandLane c4803be323 fix #13527 (#13579)
Co-authored-by: Lane <>
2024-02-07 14:08:06 +00:00
Hariom BalharaandUdit Takkar 34d360a4ec fix: Reduce payload in event-type/getByViewer (#13580)
* Avoid selecting unused props

* Reduce payload

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-07 09:40:45 -03:00
0e6f44e2b9 feat: email confirmation on change (#13443)
* feat:show old and new email in confirmation dialog

* fix: update i18n

* chore: update styles

* wip: change email upon verification

* fix: email being changed w/o need to logout

* chore: tidy up - remove update sessison (WIP) will try do this serverside

* wip: try new approach of serverside call to nextjs then client update

* fix: tests

* fix: update without logout

* i18n: use i18n in toast

* fix: locale ready toast

* fix: restore email api template

* fix: revert changes from wrong branch

* fix: restore yarn.lock

* feat: happy e2e path

* fix: verification disabled e2e tests

* fix:move toast

* chore: cleanup early return

* fix: await input selector

* Update apps/web/pages/auth/verify-email-change.tsx

* fix: feedback

* fix: update the way we update session

* fix: reset password -> email verified

* fix: email change toast failure

* tests: add tests for error path

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-07 10:31:00 +00:00
Somay ChauhanandKeith Williams e377997d49 fix: filtering events bug in bookings page (#13447)
* fix: filtering events bug in bookings page

* fix: handle edge case if we change hosts in event-settings/assignment

* update: handled edge case

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-07 10:01:48 +00:00
Somay ChauhanandKeith Williams a0451e34c1 fix: Ensure event type duplication respects private link setting (#13438)
* fix: Ensure event type duplication respects private link setting

* chode refactor

* Fix: Handler throwing error on creating duplicate of a collective event having private-link switched on.

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-07 09:30:28 +01:00
Omar López d2b4760e3a refactor: removed isNotAnApiCall (#13568) 2024-02-07 04:43:36 -03:00
8469c91973 feat: integrate formbricks for team disband action (#13362)
* feat: integrate formmbricks for team disband action

* fix: attribute typing & duplicate action

* fix: track action uniformly across repo

* fix: move action tracking on success of team disband mutation

* fix: yarn lock file

* feat: bump formbricks/js to latest version

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-07 04:14:26 -03:00
Keith Williams 0990549e7f perf: Don't process date overrides outside of requested boundaries (#13495)
* perf: Don't process date overrides outside of requested boundaries

* Added ability to skip override calculations when not used

* Only instantiating utc date for overrides once

* Removed performance tracing

* Cleanup

* Fixed broken tests

* Fix type

* In process of fixing types

* Types

* Fixed types

* Removed unnecessary performance.now() call

* Changed override date check to use isBetween

* Attempting to fix availability test

* Put code back to test availability

* Put back isBetween because confirmed it wasn't triggering the new test failures

* Removed date check to see if this is breaking it

* Put the code back since the test failed without it too

* Skipping test to unblock pipeline unless research is done

* Removed steps to test

* Removing the piece around the troubleshooter

* 1 more try

* Adding an extra wait

* Took out the boolean check to see if that's the issue

* Put back the check

* Trying to isolate the problem

* Changed how the test loads the troubleshooter

* Changed to OR to capture range better

* Using the proper date instead of 1/1/1970

* Update availability.e2e.ts
2024-02-07 03:21:43 +00:00
Omar López e884348807 chore: adds failsafe auto migrations back (#13569) 2024-02-06 22:59:01 -03:00
zomars 5684ce2d28 Update action.yml
Updates actions
2024-02-06 17:39:19 -07:00
zomars bd8ac12f14 Update yarn.lock 2024-02-06 17:15:21 -07:00
Keith Williams 512628e8ab perf: Load bookings used for limits in bulk (#13474) 2024-02-06 16:56:05 -07:00
Keith Williams 903a154dbc v3.7.14
release.yml / release (push) Canceled after 0s
2024-02-06 19:52:05 -03:00
Carina WollendorferandCarinaWolli 1a3d54a0d0 fix: org user event types with same event slug (#13571)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-02-06 22:50:02 +00:00
Udit Takkar a5970cb1a8 fix: 2FA falky test (#13567)
release.yml / release (push) Canceled after 0s
* fix: 2FA falky test

* chore: increasing limit
2024-02-06 20:07:09 +00:00
Keith Williams 925dc04f1c v3.7.13 2024-02-06 17:07:05 -03:00
271f31730d fix: recurring round robin events (#13471)
* use same lucky user + check availability

* add tests for recurring round robin events

* only use luckyUsers for recurring bookings

* don't check availability for all recurring dates

* code clean up

* pass schedulingType

* fix type error

* add availability check for fixed hosts + test

* fix type error

* implement feedback

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-06 18:18:42 +00:00
Udit Takkar 684f4a795c fix: locale flaky tests (#13564) 2024-02-06 16:52:21 +00:00
836429348c fix: 'Invalid event length' error for 1 hour dynamic events (#13522)
* fix #13507 'Invalid event length' when creating dynamic events with a 1 hour duration

* Fix failing e2e tests by dynamically injecting lengths for dynamic events

--amend 1: misc indentation cleanup
--amend 2: Added condition to only update event metadata when slug is dynamic and multipleDuration is empty

* Override the multipleDuration property while constructing the dynamicEvent object itself

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-02-06 15:55:04 +00:00
Udit TakkarandKeith Williams a55138348f fix: Flakiness in location tests (#13557)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-06 11:54:35 -03:00
66b5dfd796 fix: payload cleanup (#13552)
* fix: payload cleanup

* Update eventType.ts

* Send whitelisted fields everywhere

* Removed a user select

* Removed extra usage of users

* Put back user field with fewer selects

* Added user pick

* Removed more users selects

* Another select removal

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-06 11:20:08 -03:00
bba68e09a9 fix: add emailVerified in seed script (#13530)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2024-02-06 10:33:47 -03:00
Hariom Balhara bab182960f Fix admin being able to update owner details (#13534) 2024-02-06 12:52:40 +00:00
Udit TakkarandKeith Williams f09153b3bb fix: don't display setup page icon for these apps (#13537)
* fix: don't display setup page icon for these apps

* chore: make it optional

* chore: feedback

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-06 04:10:22 +00:00
3e492eb17b fix: event-type listing not showing existing event-types when an existing user is invited to an organization (#13524)
* Fix event-type listing not working

* Automatic reload of the app when being added to a different organization

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-02-06 03:52:24 +00:00
Omar López fe5fe8d53b v3.7.12
release.yml / release (push) Canceled after 0s
2024-02-05 13:25:43 -07:00
Omar LópezandKeith Williams 0cc5937104 chore: uses safeGet to be more explicit (#13543)
* chore: uses safeGet to be more explicit

* Update apps/web/middleware.ts

* Update apps/web/middleware.ts

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-05 20:20:18 +00:00
Joe Au-Yeung fde3b49126 Add null upId and profile (#13545) 2024-02-05 19:54:42 +00:00
Keith Williams 556787b93b perf: yarn patch dayjs (#13542)
* perf: Use overriden tz function

* Renamed .js to .ts

* Trying to use a type file for timezone

* perf: Yarn patch dayjs

* Fixed import casing
2024-02-05 19:53:15 +00:00
Peer Richelsen dcb244d7be fixed france i18n (#13536) 2024-02-05 18:05:02 +00:00
Alex van AndelandKeith Williams 8d7fb75f71 fix: Refactored availability tests for less flakiness (#13518)
* fix: Refactored availability tests for less flakiness

* Fixed type error

* Removed redundant locator

* fix: Commit updated yarn.lock file

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-05 16:00:49 +00:00
4fe75771b8 fix: Broken routing form public links(old links) on migrating the owner user to an organization (#13416)
* Fix routing-form access restrictions

* Update packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx

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

* Fix routing form access issue for users who are never migrated

* Improve comment

* wip

* Fix event-type listing not working

* Automatic reload of the app when being added to a different organization

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-02-05 15:51:30 +00:00
Alex van Andel 63f537a08f fix: wrap isSignupDisabled in try/catch 2024-02-05 14:26:40 +00:00
Crowdin Bot bed593c835 New Crowdin translations by Github Action 2024-02-04 19:22:35 +00:00
Alex / KATT f367c5ed3d chore: use one yarn version (#13517) 2024-02-04 19:19:46 +00:00
zomars af1382c5ba feat: adds signup disabling middleware 2024-02-03 14:47:51 -07:00
Tangerine Kugelmann eeb0fc795d chore: create security.txt (#13454) 2024-02-03 14:15:37 +00:00
4aa040d96e feat: routing form fields as variables in route (#13519)
* add UI for setting up custom event typ url

* extract variables when processing route

* remove console.log

* fix some UI issues

* code clean up

* add translation

* fix type error

* Update packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx

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

* make variable check case insensitive

* add function

* code clean up

* code clean up

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2024-02-02 19:25:37 -03:00
12466fb064 feat: New Organization Schema along with the ability to invite existing users to an organization (#13002)
* Add profile table

Copy to profile table

Update schema

Migrations

* wip

* Get switcher working

* Add organization seeder

* wip

* Remove dead code

* Fix unit tests

* relevantProfile -> profile

* cache bust

* One more comment

* Squash migrations

* Copy to profile table

* Update schema

* Fix existing user invite flow

* Add profile table

* Copy to profile table

* Update schema

* Format

* Add profile table

Copy to profile table

Update schema

Migrations

* Format

* wip

Get switcher working

Add organization seeder

wip

Remove dead code

Fix unit tests

relevantProfile -> profile

cache bust

One more comment

Squash migrations

Copy to profile table

Update schema

Fix existing user invite flow

Add profile table

Copy to profile table

Update schema

chore: small improvements to signup (#13227)

* removed features on mobile, better background color

* nit

* dark mode nits

New Crowdin translations by Github Action

fix: event reschedule workflow trigger (#13122)

* fix event reschedule workflow trigger

* uncomment previously failed test

* review fix

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>

chore: added community support to help (#13213)

refactor: Abstract `handleSeats` from `handleNewBooking` (#12062)

* Refactor createBooking

* Type fix

* Abstract handleSeats

* Create Invitee type

* Create OrganizerUser type

* Abstract addVideoCallDataToEvt

* Abstract createLoggerWithEventDetails

* Abstract `handleAppStatus` from handler

* Create ReqAppsStatus type

* Move `deleteMeeting` and `getCalendar`

* Set parameters for `handleSeats`

* Typescript refactor

* Change function params from req

* Type fix

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Erik <erik@erosemberg.com>

fix: Dynamic group booking  org-migration redirect (#13223)

* fix: Dynamic group booking redirect

* Fix flaky private team test

test: Check availability tab and all your functions (teste2e-availability) (#12434)

* add changes

* add changes

* add changes

* add changes

* Requested changes

* Change way to grab element

---------

Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

fix: Reschedule a cancelled booking (#13170)

* fix: Reschedule a cancelled booking

* canceled instead of cancelled

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

v3.7.0

test: In Team Event type when location is Organizer Default App and U… (#12939)

* test: In Team Event type when location is Organizer Default App and User Metadata is emoty

* chore: use variable

* fix: type error

test: add test for invalid cal video (#13103)

* test: add test for invalid cal video

* chore: add confirm

* chore: fix type error

* chore: update code

* test: add unit test

* chore: type error

* chore

* chore: type

* chore: fix test

* chore

---------

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

chore: add error message for no availability (#13230)

* chore: add error message for no default user availability

* chore: check only availability

* chore: change message

* chore: add eventType

fix: Team revert with members (#13228)

refactor: Refactor seats logic (#12905)

* Refactor createBooking

* Type fix

* Abstract handleSeats

* Create Invitee type

* Create OrganizerUser type

* Abstract addVideoCallDataToEvt

* Abstract createLoggerWithEventDetails

* Abstract `handleAppStatus` from handler

* Create ReqAppsStatus type

* Move `deleteMeeting` and `getCalendar`

* Set parameters for `handleSeats`

* Typescript refactor

* Change function params from req

* Type fix

* Move handleSeats

* Abstract lastAttendeeDeleteBooking

* Create function for rescheduling seated events

* Fix imports on reschedule seats function

* Fix imports

* Import handleSeats function

* Fix rescheduleUid type

* Refactor owner reschedule to new time slot

* Refactor combine two booking times together

* Reschedule as an attendee

* Refactor createNewSeat

* Remove old handleSeats

* Remove lastAttendeeDeleteBooking from handleNewBooking

* Test for new attendee right params are passed

* Unit test params for reschedule

* Typo fix

* Clean up

* Create new seat test

* Test when attendee already signs up for booking

* Type fix

* Test reschedule move attendee to existing booking

* On reschedule create new booking

* Test on last attendee cancel booking

* Owner reschedule to new time slot

* Owner rescheduling, merge two bookings together

* Test: when merging more than available seats, then fail

* Test: fail when event is full

* Remove duplicate E2E tests

* Clean up

* Rename `addVideoCallDataToEvt` to `addVideoCallDataToEvent`

* Refactor `calcAppsStatus`

* Assign `evt` to resutl of `addVideoCallDataToEvent`

* Use prisma.transaction when moving attendees

* Clean create seat call

* Use ErrorCode enum

* Use attendeeRescheduledSeatedBooking function

* Await function

* Prevent double triggering of workflows

* Use inviteeToAdd in createNewSeat

* Remove unused error code

* Remove old handleSeats file

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Erik <erik@erosemberg.com>

feat: Allow admins to filter bookings only by attendee emails (#13236)

* chore: Allow admins to filter bookings only by attendee emails

* Fix type error

Fixed [CAL-2930] Org branding isnt centered in shell #13225 (#13237)

Add profile table

Copy to profile table

Update schema

Migrations

Format

* wip

* Add profile relation everywhere

* Fix schema

* Fix invite handler

* Add profileId during eventTypec reation

* Get dashboard working with profile relatioon

* Add workflow create repository

* Add profileId to host

* Add profileId to credential table

* Add destinationCalendar repository

* Add profileId to membership

* Revert all profile related changes

* Add acme and dunder

* Fix membership profileId usgae

* Various fixes

* Some renaming

* Fix failing tests

* Debugging next-aut

* Consolidate migrations

* Testing fixes

* Fix unit tests

* Self review feedback

* PR feedback

* Fix orgMigration as well

* Add profile schema and copy users to it

* Remove migrations from here

* Fix type issues

* Fix type error after main merge

* Remove movedFromUserId

* Add missing migration file

* Self review addressed

* Joe's review addressed

* Fix profile switcher

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-02-02 21:34:13 +00:00
Carina Wollendorfer 42a3448abb fix translation (#13516) 2024-02-02 16:01:28 +00:00
Peer RichelsenandKeith Williams 92ff49f6a1 replaced border-neutral-300 with border-subtle (#13500)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-02 15:49:41 +00:00
Udit Takkar c4aa555610 fix: remove check for location (#13493) 2024-02-02 15:41:27 +00:00
Somay Chauhan a45d77de51 fix: initialized flags on event-types page (#13469)
* fix: initialized flags on event-types page

* changes

* chore: removed `managed-event-types` flag as no longer needed

* revert: `20230404202721_add_feature_flag_managed_event_types` migration
2024-02-02 15:35:00 +00:00
ErikandKeith Williams 0421dd401a feat: API wrapper for invite members. (#13496)
* chore: API

* chore: remove

* chore: Update

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-02-02 12:27:17 -03:00
Hariom Balhara 094189fc3b Correcly identify the user during signup (#13506) 2024-02-02 15:18:00 +00:00
Abhijeet Singhandsupalarry 677a964d4f feat: Added new API route to get event-types using slug (#13374)
* added new api route /event-types/bySlug/{slug}

* refactor: schema query slug

* refactor: event-types/bySlug to event-types/by-slug route

* feat: allow admins to fetch event types on behalf of others

* fix: auth middleware

* added slug query param to main event-type api route

* removed the /by-slug route

* refactor logic

* remove unwanted changes

* updated swagger api doc

---------

Co-authored-by: supalarry <lauris.skraucis@gmail.com>
2024-02-02 15:10:43 +00:00
sean-brydon 2357c919c1 fix: weekly view booker blocked slots (#13511) 2024-02-02 15:08:24 +00:00
Hichem Fantar 27b7cccfa6 fix: syntax error in WordPress plugin (#13482) 2024-02-02 14:46:46 +00:00
Crowdin Bot ab2354f2a4 New Crowdin translations by Github Action 2024-02-02 14:31:56 +00:00
Alex van Andel 538e5017c5 fix: Skip booking limits for now (#13510) 2024-02-02 11:27:36 -03:00
2e3060b738 docs: Adds deployment guides for AWS, GCP and Azure (#12656)
* Deployment Steps

* Update deployment.md

* Deployment steps

* Update deployment.md

* Deployment Steps

* Update deployment.md

* Update yarn.lock

* Update yarn.lock

* Update and rename deployment.md to README.md

---------

Co-authored-by: Nathaniel-Nirmal <na453@cornell.edu>
Co-authored-by: Sriya Challa <100821752+sriyachalla@users.noreply.github.com>
Co-authored-by: Nathaniel-Nirmal <142919730+Nathaniel-Nirmal@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2024-01-31 11:24:45 +00:00
Peer Richelsen e9d9aebd88 chore: cal.ai try out new gpt-4-0125-preview (#13461) 2024-01-31 06:42:25 -03:00
Omar López 7d1c69801e chore: optimizing bundle size (#13473)
* WIP

* Tree shaking
2024-01-30 19:49:29 -03:00
Peer Richelsen 4235b32349 chore: added 90 min to + group links (#13475) 2024-01-30 20:28:48 +00:00
Omar López 2ca031e25c v3.7.11
release.yml / release (push) Canceled after 0s
2024-01-30 09:33:30 -07:00
Omar López 9f734b8964 fix: tailwind config for web modules (#13466) 2024-01-30 15:26:41 +00:00
Peer Richelsen d6927c5049 chore: fixed theme colors (#13463)
the palette has changed so the theme has to be adjusted for pwa mode
2024-01-30 12:04:57 +00:00
Keith Williams 910f5f906f v3.7.10
release.yml / release (push) Canceled after 0s
2024-01-30 08:27:06 -03:00
Peer RichelsenandHariom 71f955e256 feat: salesroom app (#13277)
* added salesroom app

* fixes to screenshots

* new screenshots and description

* Add missing update to yarn.lock

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-01-30 11:25:29 +00:00
sean-brydon 5e60654aa9 fix: make show more banner static > absolute to allow embed auto height (#13457)
* fix: make show more banner static > abosulte to allow embed auto height

* fix: hide on mobile
2024-01-30 08:48:08 +00:00
Somay Chauhan b6175b3dde fix: Pay to book for an event that no longer has payments (#13432) 2024-01-29 22:44:45 +00:00
a0a731b357 chore: adds future opt-in banner for ADMINS only (#13315)
* chore: adds future opt-in banner for ADMINS only

* Adds banner to shell

* Update teams.e2e.ts

* fix: e2e ab tests

* Update ab-tests-redirect.e2e.ts

* Discard changes to apps/web/playwright/ab-tests-redirect.e2e.ts

* Discard changes to apps/web/playwright/app-store.e2e.ts

* Discard changes to apps/web/playwright/event-types.e2e.ts

* Discard changes to apps/web/playwright/payment.e2e.ts

* Discard changes to apps/web/playwright/settings-admin.e2e.ts

* Discard changes to apps/web/playwright/teams.e2e.ts

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-29 13:20:01 -07:00
Omar López fdaa6961f8 fix: Adds missing rate limiting headers (#13430) 2024-01-29 12:26:11 -07:00
Benny Joo d91e74ec13 chore: module based file structure for /video group (#13453)
* chore: module based file structure for /video group

* fix

* fix
2024-01-29 18:59:24 +00:00
Keith Williams 0af780aac9 fix: before/after event buffers not rendering correctly (#13451) 2024-01-29 15:29:26 +00:00
41b0589ab0 fix: seated event custom questions (#12779)
* fix: seated event custom questions

* updating tests & code refactor

* addition of prisma custom json schema and type fixes

* attendeeName parsing refactor

* response parsing fix

* response parsing fix

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-29 14:37:52 +00:00
Somay ChauhanandUdit Takkar 00e96ac7ad fix: show leave site alert for instant meetings only (#13448)
* fix: leave site alert

* fix: only use expiry time

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-01-29 13:40:47 +00:00
Keith Williams 63feb721ea fix: Deployments on main (#13450) 2024-01-29 10:00:53 -03:00
Somay ChauhanandUdit Takkar 43c1bd63ca fix: destination calendar selection resetting to default on page reload (#13440)
* fix: destination calendar selection resetting to default on page reload

* fix: duplicate destination calendar settings on duplicate event

* fix: zod error in test

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-01-29 10:03:26 +00:00
8bc265faaa fix: When declining OAuth return to previous page (#13189)
* Get `onErrorReturnTo` URL

* If no code return to previous page

* On invalid permissions, return to previous page

* When not enough permissions given, invalidate the credential created

* Type fix

* Stripe go back to previous page on cancel

* Clean up console.logs

* Add early returns when redirecting

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-01-29 05:51:53 -03:00
Morgan f33ea91ab0 chore: move getFixedT to web (#13446) 2024-01-29 08:44:15 +00:00
cc0ff17d97 test: Create E2E tests for a collective event type (teste2e-bookCollective) (#12565)
* refactor

* add localize / refactor

* add changes

* fix conflicts

* replace text by testId

* refactor

* Update regularBookings.ts

* fix conflicts

* fix failing check

* fix failing test

---------

Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
2024-01-28 20:55:17 -03:00
Crowdin Bot 4d128971bb New Crowdin translations by Github Action 2024-01-28 21:09:13 +00:00
Udit Takkar f9f82283fb chore: add logs for debugging (#13441) 2024-01-28 21:06:44 +00:00
Crowdin Bot 111135a605 New Crowdin translations by Github Action 2024-01-28 08:13:15 +00:00
Somay Chauhan 04e577582d fix: copy private link tooltip (#13437) 2024-01-28 08:10:10 +00:00
neo773 6643d3c20b fix: prefill avatar when signing up (#13427) 2024-01-27 14:24:31 +00:00
Somay Chauhan 3c42301c19 fix: "Hide notes in calendar" keeps reverting to off despite saving (#13434) 2024-01-27 12:04:15 +00:00
Somay Chauhan 20517be280 fix: app crash on clicking on an event (#13435) 2024-01-27 12:03:23 +00:00
fa45a85088 Create RTL tests for TimezoneSelect component (#10765)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-01-27 07:59:44 -03:00
Benny Jooandzomars c839e40a3e chore: [app-router-migration 26] Migrate "/[user]" page (#13057)
Co-authored-by: zomars <zomars@me.com>
2024-01-27 10:57:43 +00:00
9422e28598 test: check all advanced options in a managed event-type (teste2e-managedOptions) (#12294)
* check all advanced options in a managed event-type

* Delete apps/web/playwright/booking/managedBookingAllQuestion.e2e.ts

* Delete apps/web/playwright/managedBooking/allQuestions.e2e.ts

* Refactor

* Refactor

* check all advanced options in a managed event-type

* check all advanced options in a managed event-type

* Update regularBookings.ts

* Update regularBookings.ts

---------

Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-27 07:50:38 -03:00
d804b3947d chore: [app-router-migration 36] Migrate /org/* page group (#13202)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Pradumn Kumar <pradumn@tealfeed.com>
Co-authored-by: zomars <zomars@me.com>
2024-01-26 13:06:02 -07:00
David SomersandKeith Williams 3fa710fc1c Fix typo in webhook delete response (#13409)
It's a webhook, not a schedule.

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-26 19:29:24 +00:00
f9359aaa7b feat: added ability to assign event to all members (#13219)
* feat: added ability to assign event to all members

* fix: types

* chore: incorporate assignment for collective and round robin event sas well

* fix: update event types query

* feat: round robin hosts

* fix: update

* fix: update

* fix: assignAllTeamMembers issue

* design fixes

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-01-26 17:37:38 +00:00
Carina WollendorferandCarinaWolli 7c7ca79d48 remove duplicate confirmBooking (#13423)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-01-26 16:27:18 +00:00
DmytroHryshyn 753cb5803d chore: module based file structure: event types (#13404) 2024-01-25 18:37:26 -07:00
Omar López 210bc9fc8c chore: Proposal for new module based file structure (#13396) 2024-01-25 17:31:08 -07:00
Crowdin Bot 3d8b5390df New Crowdin translations by Github Action 2024-01-25 22:30:50 +00:00
zomars a477f337dc fix: login flaky test 2024-01-25 15:27:45 -07:00
9db9204088 test: Check the webhook event tab and your funtionalities (#12433)
* Check the webhook event tab and your funtionalities

* Check the webhook event tab and your funtionalities

* add changes

* add changes

* add changes

* add changes

* add changes

* add changes

* fix tests

* Refactor

---------

Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-01-25 15:50:11 -03:00
Crowdin Bot fe46b81c63 New Crowdin translations by Github Action 2024-01-25 17:54:39 +00:00
Keith Williams 17db644be1 v3.7.9
release.yml / release (push) Canceled after 0s
2024-01-25 14:51:08 -03:00
e4037be79a feat: Limit the amount of days to load for a schedule (#13282)
* feat: Disallow next month prefetching in booker

* renamed to include NEXT_PUBLIC

* fixed boolean expression

* Now using a variable length of days to load

* Type fixes

* Now opening all dates to be clickable

* Added turbo.json var and updated example env

* Fixed issue with selectedDate being null when first loading the page

* Updated description of variable

* Made constant for the booking days

* Type fix

* Setting the days to load for weekly view to 7

* feat: add ui and ability to fetch more on click

* feat: add ui and ability to fetch more on click

* fix: loader

* fix: banner detection

* Update packages/features/bookings/Booker/Booker.tsx

* fix: types

* Not showing HavingTroubleFindingTime component when the schedule is loading

* Only enabled on monthly view

* Correctly load initial date

* Type fix

* Fixed bugs around triggering multiple requests

* Added case for when there is no selectedDate but the month has been specified

* Types

* More types

* Check for undefined

* More typing

* More typing

---------

Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-01-25 14:48:46 -03:00
DmytroHryshyn ebe6a60366 fix insights page (#13390) 2024-01-24 14:22:09 -07:00
zomars fa9c6ea0ef fix: flaky test 2024-01-24 14:21:06 -07:00
Keith Williams 9b22f44cba v3.7.8
release.yml / release (push) Canceled after 0s
2024-01-24 16:01:51 -03:00
Morgan c84f75df99 fix: Booker shows toggle external calendar on external links (#13395) 2024-01-24 15:38:17 -03:00
Crowdin Bot 8f9a4fc78f New Crowdin translations by Github Action 2024-01-24 16:06:17 +00:00
7b30ade284 test: Create E2E tests to check the responsive design for a regular booking and edit a regular event type (responsiveLayoute2e) (#12432)
* add tests for responsive layout

* test: Create E2E tests to check the responsive design for a regular booking and edit a regular event type

* test: Create E2E tests to check the responsive design for a regular booking and edit a regular event type

* test: Create E2E tests to check the responsive design for a regular booking and edit a regular event type

* Create tests

* Create tests

* Create tests

* add changes

---------

Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-24 13:02:12 -03:00
Keith WilliamsandJoe Au-Yeung 24a4646fdc fix: sync/async calls in getSchedule (#13387)
* Fixed the sync/async calls

* fixing type errors

* fixing more type errors

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-01-24 10:05:25 -05:00
Keith Williams 0860036607 v3.7.7
release.yml / release (push) Canceled after 0s
2024-01-24 11:06:38 -03:00
Peer RichelsenandOmar López d080a0ef2d feat: notion calendar (#13299)
* replaced cron with notion calendar

* fixed app store for cron

* fixed slug

* Apply suggestions from code review

---------

Co-authored-by: Omar López <zomars@me.com>
2024-01-24 12:10:15 +00:00
a6eab74fd1 feat: make org members list private (#13373)
* feat: make org members list private

* fix: add isOrg to MakeTeamPrivateSwitch in other-team-members-view

---------

Co-authored-by: supalarry <lauris.skraucis@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-01-24 11:28:34 +00:00
0a2e275908 fix: UI mismatch with form state (#10651)
* fix: UI mismatch with form state

* Fix: Sync form state with ui for more fields & slight code refactor

* Ensure Stripe app is enabled to disable seats

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2024-01-24 10:55:31 +00:00
Crowdin Bot 3b05ebc5b5 New Crowdin translations by Github Action 2024-01-24 10:46:13 +00:00
Omar López e5b4d2fb80 fix: API slots endpoint return format (#13377) 2024-01-24 10:43:13 +00:00
Syed Ali Shahbaz 00a2a3d8a8 Fix slot wrapper (#13379) 2024-01-24 07:01:57 +00:00
30ba168fc3 chore: bump @trpc/* and @tanstack/react-query (#13335)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Gergő Móricz <mo.geryy@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-23 16:42:29 -07:00
SashaandDmytro Hryshyn 12eec186b2 chore: [app-router-migration 31] migrate the "/auth/setup" page (#13196)
* wip

* chore: setup page migration

* fix: conventions

* fix: convention

* use client

* fix: rebase on 13.5

---------

Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com>
2024-01-23 18:05:47 -03:00
MorganandOmar López 4417d649ea refactor: remove @calcom/features dependency to @calcom/web (#13365)
* refactor: remove @calcom/features dependency to @calcom/web

* refactor: remove IS_VISUAL_REGRESSION_TESTING from web

* fix: wrong imports

---------

Co-authored-by: Omar López <zomars@me.com>
2024-01-23 17:17:36 +00:00
797 changed files with 23892 additions and 10798 deletions
+7 -2
View File
@@ -23,6 +23,9 @@ CALCOM_LICENSE_KEY=
# - DATABASE ************************************************************************************************
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
# Needed to run migrations while using a connection pooler like PgBouncer
# Use the same one as DATABASE_URL if you're not using a connection pooler
DATABASE_DIRECT_URL="postgresql://postgres:@localhost:5450/calendso"
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
INSIGHTS_DATABASE_URL=
@@ -134,8 +137,8 @@ NEXT_PUBLIC_SENDGRID_SENDER_NAME=
NEXT_PUBLIC_SENTRY_DSN=
# Formbricks Experience Management Integration
FORMBRICKS_HOST_URL=https://app.formbricks.com
FORMBRICKS_ENVIRONMENT_ID=
NEXT_PUBLIC_FORMBRICKS_HOST_URL=https://app.formbricks.com
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=
FORMBRICKS_FEEDBACK_SURVEY_ID=
# AvatarAPI
@@ -236,6 +239,8 @@ CSP_POLICY=
EDGE_CONFIG=
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD=0 # Override the booker to only load X number of days worth of data
# Control time intervals on a user's Schedule availability
NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL=
+1
View File
@@ -18,6 +18,7 @@ runs:
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
key-2: ${{ github.event.pull_request.number || github.ref }}
DATABASE_URL: ${{ inputs.DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ inputs.DATABASE_URL }}
E2E_TEST_CALCOM_QA_EMAIL: ${{ inputs.E2E_TEST_CALCOM_QA_EMAIL }}
E2E_TEST_CALCOM_QA_PASSWORD: ${{ inputs.E2E_TEST_CALCOM_QA_PASSWORD }}
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ inputs.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
+8
View File
@@ -11,6 +11,9 @@ jobs:
services:
postgres:
image: postgres:12.1
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
POSTGRES_USER: postgres
POSTGRES_DB: calendso
@@ -18,6 +21,9 @@ jobs:
- 5432:5432
mailhog:
image: mailhog/mailhog:v1.0.1
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
ports:
- 8025:8025
- 1025:1025
@@ -39,6 +45,7 @@ jobs:
- uses: ./.github/actions/cache-db
env:
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
@@ -50,6 +57,7 @@ jobs:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
+4
View File
@@ -11,6 +11,9 @@ jobs:
services:
postgres:
image: postgres:12.1
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
POSTGRES_USER: postgres
POSTGRES_DB: calendso
@@ -41,6 +44,7 @@ jobs:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
+7
View File
@@ -11,6 +11,9 @@ jobs:
services:
postgres:
image: postgres:12.1
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
POSTGRES_USER: postgres
POSTGRES_DB: calendso
@@ -18,6 +21,9 @@ jobs:
- 5432:5432
mailhog:
image: mailhog/mailhog:v1.0.1
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
ports:
- 8025:8025
- 1025:1025
@@ -44,6 +50,7 @@ jobs:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
+7
View File
@@ -11,6 +11,9 @@ jobs:
services:
postgres:
image: postgres:12.1
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
POSTGRES_USER: postgres
POSTGRES_DB: calendso
@@ -18,6 +21,9 @@ jobs:
- 5432:5432
mailhog:
image: mailhog/mailhog:v1.0.1
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
ports:
- 8025:8025
- 1025:1025
@@ -42,6 +48,7 @@ jobs:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
with:
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
organization-name: calcom
ignore-labels: "admin, app-store, ai, authentication, automated-testing, devops, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
ignore-labels: "admin, app-store, ai, authentication, automated-testing, devops, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
apply-labels-from-issue:
runs-on: ubuntu-latest
@@ -7,6 +7,7 @@ env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
+4
View File
@@ -7,6 +7,7 @@ env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
@@ -41,6 +42,9 @@ jobs:
services:
postgres:
image: postgres:12.1
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
POSTGRES_USER: postgres
POSTGRES_DB: calendso
+226 -56
View File
@@ -1,9 +1,10 @@
// This transform config was generated by Snaplet.
// Snaplet found fields that may contain personally identifiable information (PII)
// and used that to populate this file.
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path=".snaplet/snaplet.d.ts" />
// This config was generated by Snaplet make sure to check it over before using it.
import { copycat as c } from "@snaplet/copycat";
import { defineConfig } from "snaplet";
import type { Transform } from "./structure";
// c.setHashKey(REPLACE_ME_WITH_YOUR_HASH_KEY);
function hasStringProp<T extends string>(x: unknown, key: T): x is { [key in T]: string } {
return !!x && typeof x === "object" && key in x;
@@ -41,57 +42,226 @@ function replaceSensitiveKeys(record: object) {
const generateUsername = (x: string) => `${c.firstName(x)}-${c.lastName(x)}${c.int(x, { min: 2, max: 99 })}`;
const config: Transform = () => ({
public: {
ApiKey: ({ row }) => ({
hashedKey: c.uuid(row.hashedKey),
}),
App: ({ row }) => ({
keys: replaceSensitiveKeys(row.keys),
}),
Attendee: ({ row }) => ({
email: c.email(row.email),
name: c.fullName(row.name),
timeZone: c.timezone(row.timeZone),
}),
Credential: ({ row }) => ({
key: typeof row.key === "string" ? c.uuid(row.key) : replaceSensitiveKeys(row.key),
}),
EventType: ({ row }) => ({
slug: generateSlug(row.slug),
timeZone: c.timezone(row.timeZone),
eventName: c.words(row.eventName, { max: 3 }),
}),
ResetPasswordRequest: ({ row }) => ({
email: c.email(row.email),
}),
Schedule: ({ row }) => ({
timeZone: c.timezone(row.timeZone),
}),
Team: ({ row }) => ({
bio: c.sentence(row.bio),
name: c.words(row.name, { max: 2 }),
slug: generateSlug(row.slug),
}),
users: ({ row }) =>
row.role !== "ADMIN"
? {
bio: c.sentence(row.bio),
email: c.email(row.email),
name: c.fullName(row.name),
password: c.password(row.password),
timeZone: c.timezone(row.timeZone),
username: generateUsername(row.username),
}
: row,
VerificationToken: ({ row }) => ({
token: c.uuid(row.token),
}),
Account: ({ row }) => ({
access_token: c.uuid(row.access_token),
refresh_token: c.uuid(row.refresh_token),
}),
export default defineConfig({
transform: {
$mode: "unsafe",
public: {
Account({ row }) {
return {
refresh_token: c.uuid(row.refresh_token),
access_token: c.uuid(row.access_token),
expires_at: c.int(row.expires_at, {
min: 0,
max: Math.pow(4, 8) - 1,
}),
token_type: c.uuid(row.token_type),
id_token: c.uuid(row.id_token),
};
},
ApiKey: ({ row }) => ({
hashedKey: c.uuid(row.hashedKey),
note: c.fullName(row.note),
createdAt: c.dateString(row.createdAt, {
minYear: 2020,
}),
}),
App: ({ row }) => ({
keys: replaceSensitiveKeys(row.keys),
}),
App_RoutingForms_Form({ row }) {
return {
name: c.fullName(row.name),
};
},
Attendee: ({ row }) => ({
email: c.email(row.email),
name: c.fullName(row.name),
timeZone: c.timezone(row.timeZone),
locale: c.fullName(row.locale),
}),
Availability({ row }) {
return {
startTime: c
.dateString(row.startTime, {
minYear: 2020,
})
.slice(11, 19),
endTime: c
.dateString(row.endTime, {
minYear: 2020,
})
.slice(11, 19),
};
},
Booking({ row }) {
return {
title: c.fullName(row.title),
startTime: c.dateString(row.startTime, {
minYear: 2020,
}),
endTime: c.dateString(row.endTime, {
minYear: 2020,
}),
location: c.sentence(row.location),
metadata: {
[c.word(row.metadata)]: c.words(row.metadata),
},
};
},
Credential: ({ row }) => ({
key: typeof row.key === "string" ? c.uuid(row.key) : replaceSensitiveKeys(row.key),
}),
EventType: ({ row }) => ({
slug: generateSlug(row.slug),
timeZone: c.timezone(row.timeZone),
eventName: c.words(row.eventName, { max: 3 }),
currency: c.sentence(row.currency),
}),
EventTypeCustomInput({ row }) {
return {
label: c.fullName(row.label),
};
},
Feature({ row }) {
return {
slug: c.uuid(row.slug),
};
},
InstantMeetingToken({ row }) {
return {
token: c.uuid(row.token),
};
},
OAuthClient({ row }) {
return {
name: c.fullName(row.name),
};
},
OutOfOfficeEntry({ row }) {
return {
start: c.dateString(row.start, {
minYear: 2020,
}),
};
},
Payment({ row }) {
return {
amount: c.int(row.amount, {
min: 0,
max: Math.pow(4, 8) - 1,
}),
currency: c.sentence(row.currency),
data: {
[c.word(row.data)]: c.words(row.data),
},
};
},
ResetPasswordRequest: ({ row }) => ({
email: c.email(row.email),
}),
Schedule: ({ row }) => ({
name: c.fullName(row.name),
timeZone: c.timezone(row.timeZone),
}),
Session({ row }) {
return {
sessionToken: c.uuid(row.sessionToken),
};
},
Team: ({ row }) => ({
bio: c.sentence(row.bio),
name: c.words(row.name, { max: 2 }),
slug: generateSlug(row.slug),
timeZone: c.timezone(row.timeZone),
logoUrl: c.username(row.logoUrl),
}),
TempOrgRedirect({ row }) {
return {
from: c.dateString(row.from, {
maxYear: 1999,
}),
toUrl: c.city(row.toUrl),
};
},
VerificationToken({ row }) {
return {
id: c
.int(row.id, {
min: 1,
max: Math.pow(4, 8) - 1,
})
.toString(),
identifier: c.uuid(row.identifier),
token: c.uuid(row.token),
expires: c.dateString(row.expires, {
minYear: 2020,
}),
};
},
VerifiedNumber({ row }) {
return {
phoneNumber: c.phoneNumber(row.phoneNumber),
};
},
Webhook({ row }) {
return {
subscriberUrl: c.url(row.subscriberUrl),
secret: c.streetAddress(row.secret),
};
},
WebhookScheduledTriggers({ row }) {
return {
jobName: c.fullName(row.jobName),
payload: c.password(row.payload),
};
},
WorkflowStep({ row }) {
return {
sendTo: c.oneOf(row.sendTo, [
"Man",
"Woman",
"Transgender",
"Non-binary/non-conforming",
"Not specified",
]),
sender: c.oneOf(row.sender, [
"Man",
"Woman",
"Transgender",
"Non-binary/non-conforming",
"Not specified",
]),
};
},
users: ({ row }) =>
row.role !== "ADMIN"
? {
bio: c.sentence(row.bio),
email: c.email(row.email),
name: c.fullName(row.name),
password: c.password(row.password),
timeZone: c.timezone(row.timeZone),
username: generateUsername(row.username),
metadata: {
[c.word(row.metadata)]: c.words(row.metadata),
},
}
: row,
},
},
subset: {
enabled: true,
version: "3",
targets: [
{
table: "public.users",
rowLimit: 100,
},
],
keepDisconnectedTables: false,
followNullableRelations: true,
maxCyclesLoop: 0,
eager: false,
taskSortAlgorithm: "children",
},
});
export default config;
+4
View File
@@ -0,0 +1,4 @@
Contact: security@cal.com
Preferred-Languages: en
Canonical: https://cal.com/.well-known/security.txt
Policy: https://github.com/calcom/cal.com/security/policy
@@ -0,0 +1,8 @@
diff --git a/plugin/timezone.js b/plugin/timezone.js
index fb6112a96f03f53ba78162ac323dce17f635161d..991e4e5410a32f5a69cb6e0e9476a9c529657aae 100644
--- a/plugin/timezone.js
+++ b/plugin/timezone.js
@@ -1 +1 @@
-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs_plugin_timezone=e()}(this,(function(){"use strict";var t={year:0,month:1,day:2,hour:3,minute:4,second:5},e={};return function(n,i,o){var r,a=function(t,n,i){void 0===i&&(i={});var o=new Date(t),r=function(t,n){void 0===n&&(n={});var i=n.timeZoneName||"short",o=t+"|"+i,r=e[o];return r||(r=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:t,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:i}),e[o]=r),r}(n,i);return r.formatToParts(o)},u=function(e,n){for(var i=a(e,n),r=[],u=0;u<i.length;u+=1){var f=i[u],s=f.type,m=f.value,c=t[s];c>=0&&(r[c]=parseInt(m,10))}var d=r[3],l=24===d?0:d,v=r[0]+"-"+r[1]+"-"+r[2]+" "+l+":"+r[4]+":"+r[5]+":000",h=+e;return(o.utc(v).valueOf()-(h-=h%1e3))/6e4},f=i.prototype;f.tz=function(t,e){void 0===t&&(t=r);var n=this.utcOffset(),i=this.toDate(),a=i.toLocaleString("en-US",{timeZone:t}),u=Math.round((i-new Date(a))/1e3/60),f=o(a).$set("millisecond",this.$ms).utcOffset(15*-Math.round(i.getTimezoneOffset()/15)-u,!0);if(e){var s=f.utcOffset();f=f.add(n-s,"minute")}return f.$x.$timezone=t,f},f.offsetName=function(t){var e=this.$x.$timezone||o.tz.guess(),n=a(this.valueOf(),e,{timeZoneName:t}).find((function(t){return"timezonename"===t.type.toLowerCase()}));return n&&n.value};var s=f.startOf;f.startOf=function(t,e){if(!this.$x||!this.$x.$timezone)return s.call(this,t,e);var n=o(this.format("YYYY-MM-DD HH:mm:ss:SSS"));return s.call(n,t,e).tz(this.$x.$timezone,!0)},o.tz=function(t,e,n){var i=n&&e,a=n||e||r,f=u(+o(),a);if("string"!=typeof t)return o(t).tz(a);var s=function(t,e,n){var i=t-60*e*1e3,o=u(i,n);if(e===o)return[i,e];var r=u(i-=60*(o-e)*1e3,n);return o===r?[i,o]:[t-60*Math.min(o,r)*1e3,Math.max(o,r)]}(o.utc(t,i).valueOf(),f,a),m=s[0],c=s[1],d=o(m).utcOffset(c);return d.$x.$timezone=a,d},o.tz.guess=function(){return Intl.DateTimeFormat().resolvedOptions().timeZone},o.tz.setDefault=function(t){r=t}}}));
\ No newline at end of file
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs_plugin_timezone=e()}(this,(function(){"use strict";function t(){return t=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(t[i]=n[i])}return t},t.apply(this,arguments)}var e={year:0,month:1,day:2,hour:3,minute:4,second:5},n={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"},i={},r={};return function(o,a,u){var f,s=function(e,r,o){void 0===o&&(o={});var a=new Date(e),u=function(e,r){void 0===r&&(r={});var o=r.timeZoneName||"short",a=e+"|"+o,u=i[a];return u||(u=new Intl.DateTimeFormat("en-US",t({},n,{hour12:!1,timeZone:e,timeZoneName:o})),i[a]=u),u}(r,o);return u.formatToParts(a)},m=function(t,n){for(var i=s(t,n),r=[],o=0;o<i.length;o+=1){var a=i[o],f=a.type,m=a.value,c=e[f];c>=0&&(r[c]=parseInt(m,10))}var l=r[3],d=24===l?0:l,h=r[0]+"-"+r[1]+"-"+r[2]+" "+d+":"+r[4]+":"+r[5]+":000",v=+t;return(u.utc(h).valueOf()-(v-=v%1e3))/6e4},c=a.prototype;c.tz=function(e,i){void 0===e&&(e=f);var o=this.utcOffset(),a=this.toDate(),s=function(e){var i=r[e];return i||(i=new Intl.DateTimeFormat("en-US",t({},n,{timeZone:e})),r[e]=i),i}(e).format(a),m=Math.round((a-new Date(s))/1e3/60),c=u(s,{locale:this.$L}).$set("millisecond",this.$ms).utcOffset(15*-Math.round(a.getTimezoneOffset()/15)-m,!0);if(i){var l=c.utcOffset();c=c.add(o-l,"minute")}return c.$x.$timezone=e,c},c.offsetName=function(t){var e=this.$x.$timezone||u.tz.guess(),n=s(this.valueOf(),e,{timeZoneName:t}).find((function(t){return"timezonename"===t.type.toLowerCase()}));return n&&n.value};var l=c.startOf;c.startOf=function(t,e){if(!this.$x||!this.$x.$timezone)return l.call(this,t,e);var n=u(this.format("YYYY-MM-DD HH:mm:ss:SSS"),{locale:this.$L});return l.call(n,t,e).tz(this.$x.$timezone,!0)},u.tz=function(t,e,n){var i=n&&e,r=n||e||f,o=m(+u(),r);if("string"!=typeof t)return u(t).tz(r);var a=function(t,e,n){var i=t-60*e*1e3,r=m(i,n);if(e===r)return[i,e];var o=m(i-=60*(r-e)*1e3,n);return r===o?[i,r]:[t-60*Math.min(r,o)*1e3,Math.max(r,o)]}(u.utc(t,i).valueOf(),o,r),s=a[0],c=a[1],l=u(s).utcOffset(c);return l.$x.$timezone=r,l},u.tz.guess=function(){return Intl.DateTimeFormat().resolvedOptions().timeZone},u.tz.setDefault=function(t){f=t}}}));
@@ -0,0 +1,8 @@
diff --git a/plugin/timezone.js b/plugin/timezone.js
index fb6112a96f03f53ba78162ac323dce17f635161d..991e4e5410a32f5a69cb6e0e9476a9c529657aae 100644
--- a/plugin/timezone.js
+++ b/plugin/timezone.js
@@ -1 +1 @@
-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs_plugin_timezone=e()}(this,(function(){"use strict";var t={year:0,month:1,day:2,hour:3,minute:4,second:5},e={};return function(n,i,o){var r,a=function(t,n,i){void 0===i&&(i={});var o=new Date(t),r=function(t,n){void 0===n&&(n={});var i=n.timeZoneName||"short",o=t+"|"+i,r=e[o];return r||(r=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:t,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:i}),e[o]=r),r}(n,i);return r.formatToParts(o)},u=function(e,n){for(var i=a(e,n),r=[],u=0;u<i.length;u+=1){var f=i[u],s=f.type,m=f.value,c=t[s];c>=0&&(r[c]=parseInt(m,10))}var d=r[3],l=24===d?0:d,v=r[0]+"-"+r[1]+"-"+r[2]+" "+l+":"+r[4]+":"+r[5]+":000",h=+e;return(o.utc(v).valueOf()-(h-=h%1e3))/6e4},f=i.prototype;f.tz=function(t,e){void 0===t&&(t=r);var n=this.utcOffset(),i=this.toDate(),a=i.toLocaleString("en-US",{timeZone:t}),u=Math.round((i-new Date(a))/1e3/60),f=o(a).$set("millisecond",this.$ms).utcOffset(15*-Math.round(i.getTimezoneOffset()/15)-u,!0);if(e){var s=f.utcOffset();f=f.add(n-s,"minute")}return f.$x.$timezone=t,f},f.offsetName=function(t){var e=this.$x.$timezone||o.tz.guess(),n=a(this.valueOf(),e,{timeZoneName:t}).find((function(t){return"timezonename"===t.type.toLowerCase()}));return n&&n.value};var s=f.startOf;f.startOf=function(t,e){if(!this.$x||!this.$x.$timezone)return s.call(this,t,e);var n=o(this.format("YYYY-MM-DD HH:mm:ss:SSS"));return s.call(n,t,e).tz(this.$x.$timezone,!0)},o.tz=function(t,e,n){var i=n&&e,a=n||e||r,f=u(+o(),a);if("string"!=typeof t)return o(t).tz(a);var s=function(t,e,n){var i=t-60*e*1e3,o=u(i,n);if(e===o)return[i,e];var r=u(i-=60*(o-e)*1e3,n);return o===r?[i,o]:[t-60*Math.min(o,r)*1e3,Math.max(o,r)]}(o.utc(t,i).valueOf(),f,a),m=s[0],c=s[1],d=o(m).utcOffset(c);return d.$x.$timezone=a,d},o.tz.guess=function(){return Intl.DateTimeFormat().resolvedOptions().timeZone},o.tz.setDefault=function(t){r=t}}}));
\ No newline at end of file
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs_plugin_timezone=e()}(this,(function(){"use strict";function t(){return t=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(t[i]=n[i])}return t},t.apply(this,arguments)}var e={year:0,month:1,day:2,hour:3,minute:4,second:5},n={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"},i={},r={};return function(o,a,u){var f,s=function(e,r,o){void 0===o&&(o={});var a=new Date(e),u=function(e,r){void 0===r&&(r={});var o=r.timeZoneName||"short",a=e+"|"+o,u=i[a];return u||(u=new Intl.DateTimeFormat("en-US",t({},n,{hour12:!1,timeZone:e,timeZoneName:o})),i[a]=u),u}(r,o);return u.formatToParts(a)},m=function(t,n){for(var i=s(t,n),r=[],o=0;o<i.length;o+=1){var a=i[o],f=a.type,m=a.value,c=e[f];c>=0&&(r[c]=parseInt(m,10))}var l=r[3],d=24===l?0:l,h=r[0]+"-"+r[1]+"-"+r[2]+" "+d+":"+r[4]+":"+r[5]+":000",v=+t;return(u.utc(h).valueOf()-(v-=v%1e3))/6e4},c=a.prototype;c.tz=function(e,i){void 0===e&&(e=f);var o=this.utcOffset(),a=this.toDate(),s=function(e){var i=r[e];return i||(i=new Intl.DateTimeFormat("en-US",t({},n,{timeZone:e})),r[e]=i),i}(e).format(a),m=Math.round((a-new Date(s))/1e3/60),c=u(s,{locale:this.$L}).$set("millisecond",this.$ms).utcOffset(15*-Math.round(a.getTimezoneOffset()/15)-m,!0);if(i){var l=c.utcOffset();c=c.add(o-l,"minute")}return c.$x.$timezone=e,c},c.offsetName=function(t){var e=this.$x.$timezone||u.tz.guess(),n=s(this.valueOf(),e,{timeZoneName:t}).find((function(t){return"timezonename"===t.type.toLowerCase()}));return n&&n.value};var l=c.startOf;c.startOf=function(t,e){if(!this.$x||!this.$x.$timezone)return l.call(this,t,e);var n=u(this.format("YYYY-MM-DD HH:mm:ss:SSS"),{locale:this.$L});return l.call(n,t,e).tz(this.$x.$timezone,!0)},u.tz=function(t,e,n){var i=n&&e,r=n||e||f,o=m(+u(),r);if("string"!=typeof t)return u(t).tz(r);var a=function(t,e,n){var i=t-60*e*1e3,r=m(i,n);if(e===r)return[i,e];var o=m(i-=60*(r-e)*1e3,n);return r===o?[i,r]:[t-60*Math.min(r,o)*1e3,Math.max(r,o)]}(u.utc(t,i).valueOf(),o,r),s=a[0],c=a[1],l=u(s).utcOffset(c);return l.$x.$timezone=r,l},u.tz.guess=function(){return Intl.DateTimeFormat().resolvedOptions().timeZone},u.tz.setDefault=function(t){f=t}}}));
+34 -6
View File
@@ -8,14 +8,14 @@ test.describe("Org", () => {
const response = await page.goto("https://i.cal.com/embed");
expect(response?.status()).toBe(200);
await page.screenshot({ path: "screenshot.jpg" });
await expectPageToBeServerSideRendered(page);
await expectPageToBeRenderedWithEmbedSsr(page);
});
test("Org User(Peer) Page should be embeddable", async ({ page }) => {
const response = await page.goto("https://i.cal.com/peer/embed");
expect(response?.status()).toBe(200);
await expect(page.locator("text=Peer Richelsen")).toBeVisible();
await expectPageToBeServerSideRendered(page);
await expectPageToBeRenderedWithEmbedSsr(page);
});
test("Org User Event(peer/meet) Page should be embeddable", async ({ page }) => {
@@ -23,14 +23,14 @@ test.describe("Org", () => {
expect(response?.status()).toBe(200);
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
await expectPageToBeServerSideRendered(page);
await expectPageToBeRenderedWithEmbedSsr(page);
});
test("Org Team Profile(/sales) page should be embeddable", async ({ page }) => {
const response = await page.goto("https://i.cal.com/sales/embed");
expect(response?.status()).toBe(200);
await expect(page.locator("text=Cal.com Sales")).toBeVisible();
await expectPageToBeServerSideRendered(page);
await expectPageToBeRenderedWithEmbedSsr(page);
});
test("Org Team Event page(/sales/hippa) should be embeddable", async ({ page }) => {
@@ -38,9 +38,10 @@ test.describe("Org", () => {
expect(response?.status()).toBe(200);
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
await expectPageToBeServerSideRendered(page);
await expectPageToBeRenderedWithEmbedSsr(page);
});
});
test.describe("Dynamic Group Booking", () => {
test("Dynamic Group booking link should load", async ({ page }) => {
const users = [
@@ -63,12 +64,39 @@ test.describe("Org", () => {
expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(3);
});
});
test("Organization Homepage - Has Engineering and Marketing Teams", async ({ page }) => {
const response = await page.goto("https://i.cal.com");
expect(response?.status()).toBe(200);
await expect(page.locator("text=Cal.com")).toBeVisible();
await expect(page.locator("text=Engineering")).toBeVisible();
await expect(page.locator("text=Marketing")).toBeVisible();
});
test.describe("Browse the Engineering Team", async () => {
test("By User Navigation", async ({ page }) => {
await page.goto("https://i.cal.com");
await page.click('text="Engineering"');
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
});
test("By /team/engineering", async ({ page }) => {
await page.goto("https://i.cal.com/team/engineering");
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
});
test("By /engineering", async ({ page }) => {
await page.goto("https://i.cal.com/engineering");
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
});
});
});
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
async function expectPageToBeServerSideRendered(page: Page) {
async function expectPageToBeRenderedWithEmbedSsr(page: Page) {
expect(
await page.evaluate(() => {
//@ts-expect-error - __NEXT_DATA__ is a global variable defined by Next.js
return window.__NEXT_DATA__.props.pageProps.isEmbed;
})
).toBe(true);
+4 -2
View File
@@ -5,12 +5,14 @@
"author": "Cal.com Inc.",
"dependencies": {
"@calcom/prisma": "*",
"@langchain/core": "^0.1.26",
"@langchain/openai": "^0.0.14",
"@t3-oss/env-nextjs": "^0.6.1",
"langchain": "^0.0.131",
"langchain": "^0.1.17",
"mailparser": "^3.6.5",
"next": "^13.5.4",
"supports-color": "8.1.1",
"zod": "^3.22.2"
"zod": "^3.22.4"
},
"devDependencies": {
"@types/mailparser": "^3.4.0"
+1 -1
View File
@@ -1,4 +1,4 @@
import { DynamicStructuredTool } from "langchain/tools";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import type { UserList } from "~/src/types/user";
+1 -1
View File
@@ -1,4 +1,4 @@
import { DynamicStructuredTool } from "langchain/tools";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { env } from "../env.mjs";
+1 -1
View File
@@ -1,4 +1,4 @@
import { DynamicStructuredTool } from "langchain/tools";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { env } from "../env.mjs";
+1 -1
View File
@@ -1,4 +1,4 @@
import { DynamicStructuredTool } from "langchain/tools";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { env } from "../env.mjs";
+1 -1
View File
@@ -1,4 +1,4 @@
import { DynamicStructuredTool } from "langchain/tools";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { env } from "../env.mjs";
+1 -1
View File
@@ -1,4 +1,4 @@
import { DynamicStructuredTool } from "langchain/tools";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { env } from "~/src/env.mjs";
+1 -1
View File
@@ -1,4 +1,4 @@
import { DynamicStructuredTool } from "langchain/tools";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { env } from "../env.mjs";
+2 -2
View File
@@ -1,5 +1,5 @@
import { ChatOpenAI } from "@langchain/openai";
import { initializeAgentExecutorWithOptions } from "langchain/agents";
import { ChatOpenAI } from "langchain/chat_models/openai";
import { env } from "../env.mjs";
import createBookingIfAvailable from "../tools/createBooking";
@@ -13,7 +13,7 @@ import type { User, UserList } from "../types/user";
import type { WorkingHours } from "../types/workingHours";
import now from "./now";
const gptModel = "gpt-4-0613";
const gptModel = "gpt-4-0125-preview";
/**
* Core of the Cal.ai booking agent: a LangChain Agent Executor.
+7 -1
View File
@@ -13,6 +13,12 @@
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"../../packages/types/window.d.ts"
],
"exclude": ["node_modules"]
}
+2 -2
View File
@@ -1,4 +1,4 @@
import * as Sentry from "@sentry/nextjs";
import { captureException as SentryCaptureException } from "@sentry/nextjs";
import type { NextMiddleware } from "next-api-middleware";
import { redactError } from "@calcom/lib/redactError";
@@ -9,7 +9,7 @@ export const captureErrors: NextMiddleware = async (_req, res, next) => {
// middleware and the API route handler
await next();
} catch (error) {
Sentry.captureException(error);
SentryCaptureException(error);
const redactedError = redactError(error);
if (redactedError instanceof Error) {
res.status(400).json({ message: redactedError.message, error: redactedError });
+6
View File
@@ -1,6 +1,7 @@
import type { NextMiddleware } from "next-api-middleware";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { API_KEY_RATE_LIMIT } from "@calcom/lib/rateLimit";
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
@@ -9,6 +10,11 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
await checkRateLimitAndThrowError({
identifier: req.query.apiKey as string,
rateLimitingType: "api",
onRateLimiterResponse: (response) => {
res.setHeader("X-RateLimit-Limit", API_KEY_RATE_LIMIT);
res.setHeader("X-RateLimit-Remaining", response.remaining);
res.setHeader("X-RateLimit-Reset", response.reset);
},
});
await next();
+1
View File
@@ -62,6 +62,7 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
bookingLimits: true,
onlyShowFirstAvailableSlot: true,
durationLimits: true,
assignAllTeamMembers: true,
})
.merge(
z.object({
@@ -0,0 +1,7 @@
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
export const schemaQuerySlug = baseApiParams.extend({
slug: z.string().optional(),
});
+1 -1
View File
@@ -1,5 +1,5 @@
import tzdata from "tzdata";
import * as z from "zod";
import { z } from "zod";
// @note: This is a custom validation that checks if the timezone is valid and exists in the tzdb library
export const timeZone = z.string().refine((tz: string) => Object.keys(tzdata.zones).includes(tz), {
+1 -1
View File
@@ -41,6 +41,6 @@
"typescript": "^4.9.4",
"tzdata": "^1.0.30",
"uuid": "^8.3.2",
"zod": "^3.22.2"
"zod": "^3.22.4"
}
}
+2
View File
@@ -197,6 +197,7 @@ async function handler(req: NextApiRequest) {
dateTo,
eventTypeId,
userId,
returnDateOverrides: true,
});
const team = await prisma.team.findUnique({
where: { id: teamId },
@@ -232,6 +233,7 @@ async function handler(req: NextApiRequest) {
dateFrom,
dateTo,
eventTypeId,
returnDateOverrides: true,
}),
};
});
+2 -1
View File
@@ -1,5 +1,6 @@
import type { NextApiRequest } from "next";
import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
import { defaultResponder } from "@calcom/lib/server";
@@ -206,7 +207,7 @@ async function handler(req: NextApiRequest) {
const { userId, isAdmin } = req;
if (isAdmin) req.userId = req.body.userId || userId;
return await handleNewBooking(req);
return await handleNewBooking(req, getBookingDataSchemaForApi);
}
export default defaultResponder(handler);
@@ -51,7 +51,6 @@ export async function getHandler(req: NextApiRequest) {
include: {
customInputs: true,
team: { select: { slug: true } },
users: true,
hosts: { select: { userId: true, isFixed: true } },
owner: { select: { username: true, id: true } },
children: { select: { id: true, userId: true } },
+14 -3
View File
@@ -5,6 +5,7 @@ import { defaultResponder } from "@calcom/lib/server";
import type { PrismaClient } from "@calcom/prisma";
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import { schemaQuerySlug } from "~/lib/validations/shared/querySlug";
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
import getCalLink from "./_utils/getCalLink";
@@ -22,6 +23,12 @@ import getCalLink from "./_utils/getCalLink";
* type: string
* required: true
* description: Your API Key
* - in: query
* name: slug
* schema:
* type: string
* required: false
* description: Slug to filter event types by
* tags:
* - event-types
* externalDocs:
@@ -35,16 +42,20 @@ import getCalLink from "./_utils/getCalLink";
* description: No event types were found
*/
async function getHandler(req: NextApiRequest) {
const { userId, prisma } = req;
const { userId, prisma, isAdmin } = req;
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
const { slug } = schemaQuerySlug.parse(req.query);
const shouldUseUserId = !isAdmin || !slug || !!req.query.userId;
// When user is admin and no query params are provided we should return all event types.
// But currently we return only the event types of the user. Not changing this for backwards compatibility.
const data = await prisma.eventType.findMany({
where: {
userId: { in: userIds },
userId: shouldUseUserId ? { in: userIds } : undefined,
slug: slug, // slug will be undefined if not provided in query
},
include: {
customInputs: true,
team: { select: { slug: true } },
users: true,
hosts: { select: { userId: true, isFixed: true } },
owner: { select: { username: true, id: true } },
children: { select: { id: true, userId: true } },
@@ -1,4 +1,4 @@
import { CAL_URL } from "@calcom/lib/constants";
import { WEBSITE_URL } from "@calcom/lib/constants";
export default function getCalLink(eventType: {
team?: { slug: string | null } | null;
@@ -6,7 +6,7 @@ export default function getCalLink(eventType: {
users?: { username: string | null }[];
slug: string;
}) {
return `${CAL_URL}/${
return `${WEBSITE_URL}/${
eventType?.team
? `team/${eventType?.team?.slug}`
: eventType?.owner
+72
View File
@@ -0,0 +1,72 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
import type { TInviteMemberInputSchema } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema";
import { ZInviteMemberInputSchema } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema";
import type { UserProfile } from "@calcom/types/UserProfile";
import { TRPCError } from "@trpc/server";
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const data = ZInviteMemberInputSchema.parse(req.body);
await checkPermissions(req, data);
async function sessionGetter() {
return {
user: {
id: req.userId,
username: "",
profile: {
id: null,
organizationId: null,
organization: null,
username: "",
upId: "",
} satisfies UserProfile,
},
hasValidLicense: true,
expires: "",
upId: "",
};
}
const ctx = await createContext({ req, res }, sessionGetter);
try {
const caller = viewerTeamsRouter.createCaller(ctx);
await caller.inviteMember({
role: data.role,
language: data.language,
teamId: data.teamId,
usernameOrEmail: data.usernameOrEmail,
isOrg: data.isOrg,
});
return { success: true, message: `${data.usernameOrEmail} has been invited.` };
} catch (cause) {
if (cause instanceof TRPCError) {
const statusCode = getHTTPStatusCodeFromError(cause);
throw new HttpError({ statusCode, message: cause.message });
}
throw cause;
}
}
async function checkPermissions(req: NextApiRequest, body: TInviteMemberInputSchema) {
const { userId, isAdmin, prisma } = req;
if (isAdmin) return;
// To prevent auto-accepted invites, limit it to ADMIN users
if (!isAdmin && "accepted" in body)
throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" });
// Only team OWNERS and ADMINS can add other members
const membership = await prisma.membership.findFirst({
where: { userId, teamId: body.teamId, role: { in: ["ADMIN", "OWNER"] } },
});
if (!membership) throw new HttpError({ statusCode: 403, message: "You can't add members to this team" });
}
export default defaultResponder(postHandler);
+9
View File
@@ -0,0 +1,9 @@
import { defaultHandler } from "@calcom/lib/server";
import { withMiddleware } from "~/lib/helpers/withMiddleware";
export default withMiddleware()(
defaultHandler({
POST: import("./_post"),
})
);
+98
View File
@@ -0,0 +1,98 @@
import prismock from "../../../../../tests/libs/__mocks__/prisma";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test } from "vitest";
import dayjs from "@calcom/dayjs";
import handler from "./_get";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
function buildMockData() {
prismock.user.create({
data: {
id: 1,
username: "test",
name: "Test User",
email: "test@example.com",
},
});
prismock.eventType.create({
data: {
id: 1,
slug: "test",
length: 30,
title: "Test Event Type",
userId: 1,
},
});
}
describe("GET /api/slots", () => {
describe("Errors", () => {
test("Missing required data", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
});
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
{
"message": "invalid_type in 'startTime': Required; invalid_type in 'endTime': Required",
}
`);
});
});
describe("Success", () => {
describe("Regular event-type", () => {
test("Returns and event type available slots", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
eventTypeId: 1,
startTime: dayjs().format(),
endTime: dayjs().add(1, "day").format(),
usernameList: "test",
},
prisma: prismock,
});
buildMockData();
await handler(req, res);
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
{
"slots": {},
}
`);
});
test("Returns and event type available slots with passed timeZone", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
eventTypeId: 1,
startTime: dayjs().format(),
endTime: dayjs().add(1, "day").format(),
usernameList: "test",
timeZone: "UTC",
},
prisma: prismock,
});
buildMockData();
await handler(req, res);
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
{
"slots": {},
}
`);
});
});
});
});
+1 -1
View File
@@ -38,7 +38,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
},
{}
)
: availableSlots;
: availableSlots.slots;
return { slots: slotsInProvidedTimeZone };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -59,7 +59,6 @@ async function getHandler(req: NextApiRequest) {
include: {
customInputs: true,
team: { select: { slug: true } },
users: true,
hosts: { select: { userId: true, isFixed: true } },
owner: { select: { username: true, id: true } },
children: { select: { id: true, userId: true } },
@@ -5,6 +5,7 @@ import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
import type { UserProfile } from "@calcom/types/UserProfile";
import { TRPCError } from "@trpc/server";
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
@@ -21,9 +22,17 @@ const patchHandler = async (req: NextApiRequest, res: NextApiResponse) => {
id: req.userId,
username: "" /* Not used in this context */,
role: req.isAdmin ? UserPermissionRole.ADMIN : UserPermissionRole.USER,
profile: {
id: null,
organizationId: null,
organization: null,
username: "",
upId: "",
} satisfies UserProfile,
},
hasValidLicense: true /* To comply with TS signature */,
expires: "" /* Not used in this context */,
upId: "",
};
}
/** @see https://trpc.io/docs/server-side-calls */
+1 -1
View File
@@ -39,7 +39,7 @@ export async function deleteHandler(req: NextApiRequest) {
const { prisma, query } = req;
const { id } = schemaQueryIdAsString.parse(query);
await prisma.webhook.delete({ where: { id } });
return { message: `Schedule with id: ${id} deleted successfully` };
return { message: `Webhook with id: ${id} deleted successfully` };
}
export default defaultResponder(deleteHandler);
+2 -2
View File
@@ -1,5 +1,5 @@
import * as Sentry from "@sentry/nextjs";
import { init as SentryInit } from "@sentry/nextjs";
Sentry.init({
SentryInit({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
});
+2 -2
View File
@@ -1,6 +1,6 @@
import * as Sentry from "@sentry/nextjs";
import { init as SentryInit } from "@sentry/nextjs";
Sentry.init({
SentryInit({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
});
+1
View File
@@ -24,6 +24,7 @@
"@types/node": "16.9.1",
"@types/react": "18.0.26",
"@types/react-dom": "^18.0.9",
"@types/swagger-ui-react": "^4.18.3",
"typescript": "^4.9.4"
}
}
+2 -1
View File
@@ -1,9 +1,10 @@
import "highlight.js/styles/default.css";
import { type AppProps } from "next/app";
import "swagger-ui-react/swagger-ui.css";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
+1 -4
View File
@@ -1,9 +1,8 @@
import dynamic from "next/dynamic";
import type { SwaggerUI } from "swagger-ui-react";
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
const SwaggerUIDynamic: SwaggerUI & { url: string } = dynamic(() => import("swagger-ui-react"), {
const SwaggerUIDynamic = dynamic(() => import("swagger-ui-react"), {
ssr: false,
});
@@ -17,8 +16,6 @@ export default function APIDocs() {
requestSnippets={requestSnippets}
plugins={[SnippedGenerator]}
tryItOutEnabled={true}
syntaxHighlight={true}
enableCORS={false} // Doesn't seem to work either
docExpansion="list"
filter={true}
/>
+2 -3
View File
@@ -3,6 +3,8 @@ import type { NextMiddleware, NextRequest } from "next/server";
import { NextResponse, URLPattern } from "next/server";
import z from "zod";
import { FUTURE_ROUTES_ENABLED_COOKIE_NAME, FUTURE_ROUTES_OVERRIDE_COOKIE_NAME } from "@calcom/lib/constants";
const ROUTES: [URLPattern, boolean][] = [
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
@@ -25,9 +27,6 @@ const ROUTES: [URLPattern, boolean][] = [
enabled,
]);
const FUTURE_ROUTES_OVERRIDE_COOKIE_NAME = "x-calcom-future-routes-override";
const FUTURE_ROUTES_ENABLED_COOKIE_NAME = "x-calcom-future-routes-enabled";
const bucketSchema = z.union([z.literal("legacy"), z.literal("future")]);
export const abTestMiddlewareFactory =
+9 -4
View File
@@ -1,11 +1,16 @@
"use client";
import { type DehydratedState, Hydrate } from "@tanstack/react-query";
import { type DehydratedState, HydrationBoundary } from "@tanstack/react-query";
import { useMemo } from "react";
import type { DataTransformer } from "@trpc/server";
type DataTransformer = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
serialize: (data: any) => any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
deserialize: (data: any) => any;
};
export function createHydrateClient(opts: { transformer?: DataTransformer }) {
export function createHydrateClient(opts: { transformer: DataTransformer }) {
return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) {
const { state, children } = props;
@@ -16,6 +21,6 @@ export function createHydrateClient(opts: { transformer?: DataTransformer }) {
return state;
}, [state]);
return <Hydrate state={transformedState}>{children}</Hydrate>;
return <HydrationBoundary state={transformedState}>{children}</HydrationBoundary>;
};
}
+1 -4
View File
@@ -4,10 +4,7 @@ import { trpc } from "app/_trpc/client";
import { useState } from "react";
import superjson from "superjson";
import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
import { httpLink } from "@calcom/trpc/client/links/httpLink";
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
import { splitLink } from "@calcom/trpc/client/links/splitLink";
import { httpBatchLink, httpLink, loggerLink, splitLink } from "@calcom/trpc/client";
import { ENDPOINTS } from "@calcom/trpc/react/shared";
export type Endpoint = (typeof ENDPOINTS)[number];
+22 -1
View File
@@ -1,12 +1,33 @@
import { type TFunction } from "i18next";
import i18next from "i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { headers } from "next/headers";
import { constructGenericImage } from "@calcom/lib/OgImages";
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG } from "@calcom/lib/constants";
import { getFixedT } from "@calcom/lib/server/getFixedT";
//@ts-expect-error no type definitions
import config from "@calcom/web/next-i18next.config";
import { preparePageMetadata } from "@lib/metadata";
const create = async (locale: string, ns: string) => {
const { _nextI18Next } = await serverSideTranslations(locale, [ns], config);
const _i18n = i18next.createInstance();
_i18n.init({
lng: locale,
resources: _nextI18Next?.initialI18nStore,
fallbackLng: _nextI18Next?.userConfig?.i18n.defaultLocale,
});
return _i18n;
};
const getFixedT = async (locale: string, ns: string) => {
const i18n = await create(locale, ns);
return i18n.getFixedT(locale, ns);
};
export const _generateMetadata = async (
getTitle: (t: TFunction<string, undefined>) => string,
getDescription: (t: TFunction<string, undefined>) => string
@@ -0,0 +1,12 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import LegacyPage from "~/users/views/users-type-public-view";
import { getServerSideProps, type PageProps } from "~/users/views/users-type-public-view.getServerSideProps";
const getData = withAppDirSsr<PageProps>(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir(getData);
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
@@ -0,0 +1,45 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { headers, cookies } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import LegacyPage from "~/users/views/users-type-public-view";
import { getServerSideProps, type PageProps } from "~/users/views/users-type-public-view.getServerSideProps";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Record<string, string | string[]>;
searchParams: { [key: string]: string | string[] | undefined };
}) => {
const props = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
);
const { eventData, booking, user, slug } = props;
const rescheduleUid = booking?.uid;
const { trpc } = await import("@calcom/trpc");
const { data: event } = trpc.viewer.public.event.useQuery(
{ username: user, eventSlug: slug, isTeamEvent: false, org: eventData.entity.orgSlug ?? null },
{ refetchOnWindowFocus: false }
);
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
return await _generateMetadata(
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
getData,
Page: LegacyPage,
getLayout: null,
})<"P">;
+12
View File
@@ -0,0 +1,12 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import LegacyPage from "~/users/views/users-public-view";
import { getServerSideProps, type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
const getData = withAppDirSsr<UserPageProps>(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir(getData);
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
+33
View File
@@ -0,0 +1,33 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { headers, cookies } from "next/headers";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import LegacyPage from "~/users/views/users-public-view";
import { getServerSideProps, type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Record<string, string | string[]>;
searchParams: { [key: string]: string | string[] | undefined };
}) => {
const props = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
);
const { profile, markdownStrippedBio } = props;
return await _generateMetadata(
() => profile.name,
() => markdownStrippedBio
);
};
const getData = withAppDirSsr<UserPageProps>(getServerSideProps);
export default WithLayout({ getLayout, getData, Page: LegacyPage })<"P">;
+12
View File
@@ -0,0 +1,12 @@
import Setup from "@pages/auth/setup";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import type { InferGetServerSidePropsType } from "next";
import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
export default WithLayout({
getLayout: null,
Page: Setup,
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
})<"P">;
@@ -1,9 +1,10 @@
import { type PageProps, getServerSideProps } from "@pages/booking/[uid]";
import OldPage from "@pages/booking/[uid]/embed";
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import OldPage from "~/bookings/views/bookings-single-view";
import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
const getData = withAppDirSsr<PageProps>(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir(getData);
+3 -2
View File
@@ -1,4 +1,3 @@
import OldPage, { type PageProps } from "@pages/booking/[uid]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
@@ -8,9 +7,11 @@ import { cookies, headers } from "next/headers";
import { BookingStatus } from "@calcom/prisma/enums";
import { getServerSideProps } from "@lib/booking/[uid]/getServerSideProps";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import OldPage from "~/bookings/views/bookings-single-view";
import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
export const generateMetadata = async ({
params,
searchParams,
@@ -1,15 +1,13 @@
import Page from "@pages/bookings/[status]";
import { withAppDirSsg } from "app/WithAppDirSsg";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { InferGetStaticPropsType } from "next";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { APP_NAME } from "@calcom/lib/constants";
import { getStaticProps } from "@lib/bookings/[status]/getStaticProps";
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
import { validStatuses } from "~/bookings/lib/validStatuses";
import Page from "~/bookings/views/bookings-listing-view";
import { getStaticProps } from "~/bookings/views/bookings-listing-view.getStaticProps";
type Y = InferGetStaticPropsType<typeof getStaticProps>;
const getData = withAppDirSsg<Y>(getStaticProps);
@@ -24,6 +22,6 @@ export const generateStaticParams = async () => {
return validStatuses.map((status) => ({ status }));
};
export default WithLayout({ getLayout, getData, Page })<"P">;
export default WithLayout({ getLayout: null, getData, Page })<"P">;
export const dynamic = "force-static";
@@ -0,0 +1,32 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { APP_NAME } from "@calcom/lib/constants";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import EventTypePageWrapper from "~/event-types/views/event-types-single-view";
import { getServerSideProps } from "~/event-types/views/event-types-single-view.getServerSideProps";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const { eventType } = await getData(legacyCtx);
return await _generateMetadata(
(t) => `${eventType.title} | ${t("event_type")} | ${APP_NAME}`,
() => ""
);
};
const getData = withAppDirSsr(getServerSideProps);
export default WithLayout({ getLayout: null, getData, Page: EventTypePageWrapper })<"P">;
+2 -3
View File
@@ -1,8 +1,7 @@
import EventTypes from "@pages/event-types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import EventTypes from "~/event-types/views/event-types-listing-view";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -10,4 +9,4 @@ export const generateMetadata = async () =>
(t) => t("event_types_page_subtitle")
);
export default WithLayout({ getLayout, Page: EventTypes })<"P">;
export default WithLayout({ getLayout: null, Page: EventTypes })<"P">;
+3 -4
View File
@@ -1,10 +1,9 @@
import LegacyPage, { getServerSideProps } from "@pages/insights/index";
import LegacyPage from "@pages/insights/index";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { getServerSideProps } from "@lib/insights/getServerSideProps";
import { type inferSSRProps } from "@lib/types/inferSSRProps";
export const generateMetadata = async () =>
@@ -15,4 +14,4 @@ export const generateMetadata = async () =>
const getData = withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps);
export default WithLayout({ getLayout, getData, Page: LegacyPage });
export default WithLayout({ getLayout: null, getData, Page: LegacyPage });
@@ -0,0 +1,41 @@
import { type PageProps } from "@pages/team/[slug]";
import EmbedPage from "@pages/team/[slug]/embed";
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { Params, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
const props = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
);
const teamName = props.team.name || "Nameless Team";
return await _generateMetadata(
() => teamName,
() => teamName
);
};
const getEmbedData = withEmbedSsrAppDir(getData);
export default WithLayout({
Page: EmbedPage,
getData: getEmbedData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -0,0 +1,37 @@
import TeamPage, { type PageProps } from "@pages/team/[slug]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
const props = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
);
const teamName = props.team.name || "Nameless Team";
return await _generateMetadata(
() => teamName,
() => teamName
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: TeamPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -0,0 +1,44 @@
import TypePage, { type PageProps } from "@pages/team/[slug]/[type]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx as unknown as GetServerSidePropsContext);
const { eventData, user, slug, booking } = props;
const entity = eventData.entity;
const { trpc } = await import("@calcom/trpc");
const { data: event } = trpc.viewer.public.event.useQuery(
{ username: user, eventSlug: slug, isTeamEvent: false, org: entity.orgSlug ?? null },
{ refetchOnWindowFocus: false }
);
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
return await _generateMetadata(
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: TypePage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -0,0 +1,37 @@
import TeamPage, { type PageProps } from "@pages/team/[slug]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
const props = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
);
const teamName = props.team.name || "Nameless Team";
return await _generateMetadata(
() => teamName,
() => teamName
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: TeamPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -16,7 +16,8 @@ export const generateMetadata = async ({
}) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx as unknown as GetServerSidePropsContext);
const { entity, user, slug, booking } = props;
const { eventData, user, slug, booking } = props;
const entity = eventData.entity;
const { trpc } = await import("@calcom/trpc");
const { data: event } = trpc.viewer.public.event.useQuery(
{ username: user, eventSlug: slug, isTeamEvent: false, org: entity.orgSlug ?? null },
+1 -3
View File
@@ -3,8 +3,6 @@ import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { getServerSideProps } from "@lib/teams/getServerSideProps";
export const generateMetadata = async () =>
@@ -13,4 +11,4 @@ export const generateMetadata = async () =>
(t) => t("create_manage_teams_collaborative")
);
export default WithLayout({ getData: withAppDirSsr(getServerSideProps), getLayout, Page })<"P">;
export default WithLayout({ getData: withAppDirSsr(getServerSideProps), getLayout: null, Page })<"P">;
+4 -4
View File
@@ -1,11 +1,11 @@
import Page, { type JoinCallPageProps } from "@pages/video/[uid]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { APP_NAME } from "@calcom/lib/constants";
import { getServerSideProps } from "@lib/video/[uid]/getServerSideProps";
import VideosSingleView from "~/videos/views/videos-single-view";
import { getServerSideProps, type PageProps } from "~/videos/views/videos-single-view.getServerSideProps";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -13,6 +13,6 @@ export const generateMetadata = async () =>
(t) => t("quick_video_meeting")
);
const getData = withAppDirSsr<JoinCallPageProps>(getServerSideProps);
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({ getData, Page, getLayout: null })<"P">;
export default WithLayout({ getData, Page: VideosSingleView, getLayout: null })<"P">;
@@ -1,9 +1,9 @@
import Page from "@pages/video/meeting-ended/[uid]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/video/meeting-ended/[uid]/getServerSideProps";
import MeetingEnded from "~/videos/views/videos-meeting-ended-single-view";
import { getServerSideProps } from "~/videos/views/videos-meeting-ended-single-view.getServerSideProps";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -13,4 +13,4 @@ export const generateMetadata = async () =>
const getData = withAppDirSsr(getServerSideProps);
export default WithLayout({ getData, Page, getLayout: null })<"P">;
export default WithLayout({ getData, Page: MeetingEnded, getLayout: null })<"P">;
@@ -1,4 +1,3 @@
import Page from "@pages/video/meeting-not-started/[uid]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
@@ -6,7 +5,8 @@ import { WithLayout } from "app/layoutHOC";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { getServerSideProps } from "@lib/video/meeting-not-started/[uid]/getServerSideProps";
import MeetingNotStarted from "~/videos/views/videos-meeting-not-started-single-view";
import { getServerSideProps } from "~/videos/views/videos-meeting-not-started-single-view.getServerSideProps";
export const generateMetadata = async ({ params }: PageProps) => {
const booking = await prisma.booking.findUnique({
@@ -24,4 +24,4 @@ export const generateMetadata = async ({ params }: PageProps) => {
const getData = withAppDirSsr(getServerSideProps);
export default WithLayout({ getData, Page, getLayout: null })<"P">;
export default WithLayout({ getData, Page: MeetingNotStarted, getLayout: null })<"P">;
@@ -1,10 +1,11 @@
import LegacyPage from "@pages/video/no-meeting-found";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
import NoMeetingFound from "~/videos/views/videos-no-meeting-found-single-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("no_meeting_found"),
@@ -20,4 +21,4 @@ const getData = async (context: GetServerSidePropsContext) => {
};
};
export default WithLayout({ getData, Page: LegacyPage, getLayout: null })<"P">;
export default WithLayout({ getData, Page: NoMeetingFound, getLayout: null })<"P">;
+1 -2
View File
@@ -1,7 +1,6 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import LegacyPage from "@calcom/features/ee/workflows/pages/index";
export const generateMetadata = async () =>
@@ -10,4 +9,4 @@ export const generateMetadata = async () =>
(t) => t("workflows_to_automate_notifications")
);
export default WithLayout({ getLayout, Page: LegacyPage })<"P">;
export default WithLayout({ getLayout: null, Page: LegacyPage })<"P">;
@@ -16,10 +16,10 @@ import { Plus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
interface AdditionalCalendarSelectorProps {
isLoading?: boolean;
isPending?: boolean;
}
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
@@ -42,7 +42,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
return (
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
<Button StartIcon={Plus} color="secondary" {...(isPending && { loading: isPending })}>
{t("add")}
</Button>
</DropdownMenuTrigger>
+18 -16
View File
@@ -1,6 +1,6 @@
import Link from "next/link";
import type { IframeHTMLAttributes } from "react";
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
@@ -92,17 +92,19 @@ export const AppPage = ({
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
const [showDisconnectIntegration, setShowDisconnectIntegration] = useState(false);
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery(
{ appType: type },
{
onSettled(data) {
const credentialsCount = data?.credentials.length || 0;
setShowDisconnectIntegration(
data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
);
setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
},
}
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery({ appType: type });
useEffect(
function refactorMeWithoutEffect() {
const data = appDbQuery.data;
const credentialsCount = data?.credentials.length || 0;
setShowDisconnectIntegration(
data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
);
setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
},
[appDbQuery.data]
);
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
@@ -190,7 +192,7 @@ export const AppPage = ({
)}
</header>
</div>
{!appDbQuery.isLoading ? (
{!appDbQuery.isPending ? (
isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3">
@@ -211,7 +213,7 @@ export const AppPage = ({
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
loading: mutation.isPending,
};
}
return (
@@ -250,7 +252,7 @@ export const AppPage = ({
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
loading: mutation.isPending,
};
}
return (
@@ -272,7 +274,7 @@ export const AppPage = ({
)}
{dependencies &&
(!dependencyData.isLoading ? (
(!dependencyData.isPending ? (
<div className="mt-6">
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
</div>
@@ -1,5 +1,5 @@
import Link from "next/link";
import { Fragment } from "react";
import { Fragment, useEffect } from "react";
import { InstallAppButton } from "@calcom/app-store/components";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
@@ -15,10 +15,12 @@ import {
AppSkeletonLoader as SkeletonLoader,
ShellSubHeading,
Label,
showToast,
} from "@calcom/ui";
import { Calendar } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import AppListCard from "@components/AppListCard";
import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector";
@@ -28,7 +30,7 @@ type Props = {
onChanged: () => unknown | Promise<unknown>;
fromOnboarding?: boolean;
destinationCalendarId?: string;
isLoading?: boolean;
isPending?: boolean;
};
function CalendarList(props: Props) {
@@ -74,7 +76,7 @@ function ConnectedCalendarsList(props: Props) {
suspense: true,
refetchOnWindowFocus: false,
});
const { fromOnboarding, isLoading } = props;
const { fromOnboarding, isPending } = props;
return (
<QueryCell
query={query}
@@ -97,7 +99,7 @@ function ConnectedCalendarsList(props: Props) {
<div className="flex flex-col xl:flex-row xl:space-x-5">
{!!data.connectedCalendars.length && (
<div className="flex items-center">
<AdditionalCalendarSelector isLoading={isLoading} />
<AdditionalCalendarSelector isPending={isPending} />
</div>
)}
</div>
@@ -182,6 +184,15 @@ function ConnectedCalendarsList(props: Props) {
export function CalendarListContainer(props: { heading?: boolean; fromOnboarding?: boolean }) {
const { t } = useLocale();
const { heading = true, fromOnboarding } = props;
const { error, setQuery: setError } = useRouterQuery("error");
useEffect(() => {
if (error === "account_already_linked") {
showToast(t(error), "error", { id: error });
setError(undefined);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const utils = trpc.useContext();
const onChanged = () =>
Promise.allSettled([
@@ -229,7 +240,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
hidePlaceholder
value={data.destinationCalendar?.externalId}
onChange={mutation.mutate}
isLoading={mutation.isLoading}
isPending={mutation.isPending}
/>
</div>
</div>
@@ -239,7 +250,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
onChanged={onChanged}
fromOnboarding={fromOnboarding}
destinationCalendarId={data.destinationCalendar?.externalId}
isLoading={mutation.isLoading}
isPending={mutation.isPending}
/>
</>
)}
@@ -2,7 +2,7 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { doesAppSupportTeamInstall } from "@calcom/app-store/utils";
import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner";
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import { CAL_URL } from "@calcom/lib/constants";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { AppFrontendPayload } from "@calcom/types/App";
@@ -100,9 +100,9 @@ export const InstallAppButtonChild = ({
<DropdownMenuContent
className="w-auto"
onInteractOutside={(event) => {
if (mutation.isLoading) event.preventDefault();
if (mutation.isPending) event.preventDefault();
}}>
{mutation.isLoading && (
{mutation.isPending && (
<div className="z-1 fixed inset-0 flex items-center justify-center">
<Spinner />
</div>
@@ -125,7 +125,7 @@ export const InstallAppButtonChild = ({
StartIcon={(props) => (
<Avatar
alt={team.logo || ""}
imageSrc={team.logo || `${CAL_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
imageSrc={team.logo || `${WEBAPP_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
size="sm"
{...props}
/>
@@ -9,7 +9,11 @@ export default function InstalledAppsLayout({
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
return (
<Shell {...rest} hideHeadingOnMobile>
<Shell
{...rest}
title="Installed Apps"
description="Manage your installed apps or change settings"
hideHeadingOnMobile>
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
{children}
</AppCategoryNavigation>
@@ -139,7 +139,7 @@ function BookingListItem(booking: BookingItemProps) {
setRejectionDialogIsOpen(true);
},
icon: Ban,
disabled: mutation.isLoading,
disabled: mutation.isPending,
},
// For bookings with payment, only confirm if the booking is paid for
...((isPending && !paymentAppData.enabled) ||
@@ -153,7 +153,7 @@ function BookingListItem(booking: BookingItemProps) {
bookingConfirm(true);
},
icon: Check,
disabled: mutation.isLoading,
disabled: mutation.isPending,
},
]
: []),
@@ -293,7 +293,7 @@ function BookingListItem(booking: BookingItemProps) {
setViewRecordingsDialogIsOpen(true);
},
color: showCheckRecordingButton ? "secondary" : "primary",
disabled: mutation.isLoading,
disabled: mutation.isPending,
},
];
@@ -348,7 +348,7 @@ function BookingListItem(booking: BookingItemProps) {
<DialogFooter>
<DialogClose />
<Button
disabled={mutation.isLoading}
disabled={mutation.isPending}
data-testid="rejection-confirm"
onClick={() => {
bookingConfirm(false);
@@ -65,7 +65,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
<DialogClose />
<Button
data-testid="send_request"
disabled={chargeCardMutation.isLoading || chargeError}
disabled={chargeCardMutation.isPending || chargeError}
onClick={() =>
chargeCardMutation.mutate({
bookingId,
@@ -27,7 +27,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
const [rescheduleReason, setRescheduleReason] = useState("");
const { mutate: rescheduleApi, isLoading } = trpc.viewer.bookings.requestReschedule.useMutation({
const { mutate: rescheduleApi, isPending } = trpc.viewer.bookings.requestReschedule.useMutation({
async onSuccess() {
showToast(t("reschedule_request_sent"), "success");
setIsOpenDialog(false);
@@ -65,7 +65,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
<DialogClose />
<Button
data-testid="send_request"
disabled={isLoading}
disabled={isPending}
onClick={() => {
rescheduleApi({
bookingId,
+128 -113
View File
@@ -1,10 +1,9 @@
import { InfoIcon } from "lucide-react";
import dynamic from "next/dynamic";
import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import type { z } from "zod";
import type { EventNameObjectType } from "@calcom/core/event";
@@ -20,7 +19,8 @@ import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import type { EditableSchema } from "@calcom/features/form-builder/schema";
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { APP_NAME, IS_VISUAL_REGRESSION_TESTING, WEBSITE_URL } from "@calcom/lib/constants";
import { generateHashedLink } from "@calcom/lib/generateHashedLink";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
@@ -31,32 +31,27 @@ import {
Label,
SettingsToggle,
showToast,
Switch,
TextField,
Tooltip,
} from "@calcom/ui";
import { Copy, Edit, Info } from "@calcom/ui/components/icon";
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
import RequiresConfirmationController from "./RequiresConfirmationController";
const CustomEventTypeModal = dynamic(() => import("@components/eventtype/CustomEventTypeModal"));
const generateHashedLink = (id: number) => {
const translator = short();
const seed = `${id}:${new Date().getTime()}`;
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
return uid;
};
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
const { data: user } = trpc.viewer.me.useQuery();
const formMethods = useFormContext<FormValues>();
const { t } = useLocale();
const [showEventNameTip, setShowEventNameTip] = useState(false);
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!formMethods.getValues("hashedLink"));
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!formMethods.getValues("successRedirectUrl"));
const [useEventTypeDestinationCalendarEmail, setUseEventTypeDestinationCalendarEmail] = useState(
formMethods.getValues("useEventTypeDestinationCalendarEmail")
);
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const bookingFields: Prisma.JsonObject = {};
@@ -65,28 +60,31 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const selectedThemeIsDark =
user?.theme === "dark" ||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
eventType.bookingFields.forEach(({ name }) => {
formMethods.getValues().bookingFields.forEach(({ name }) => {
bookingFields[name] = `${name} input`;
});
const eventNameObject: EventNameObjectType = {
attendeeName: t("scheduler"),
eventType: eventType.title,
eventName: eventType.eventName,
host: eventType.users[0]?.name || "Nameless",
eventType: formMethods.getValues("title"),
eventName: formMethods.getValues("eventName"),
host: formMethods.getValues("users")[0]?.name || "Nameless",
bookingFields: bookingFields,
t,
};
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const [requiresConfirmation, setRequiresConfirmation] = useState(
formMethods.getValues("requiresConfirmation")
);
const placeholderHashedLink = `${WEBSITE_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const noShowFeeEnabled = eventType.metadata?.apps?.stripe?.paymentOption === "HOLD";
const noShowFeeEnabled =
formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
formMethods.getValues("metadata")?.apps?.stripe?.paymentOption === "HOLD";
useEffect(() => {
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
}, [eventType.users, hashedUrl, team?.id]);
!hashedUrl && setHashedUrl(generateHashedLink(formMethods.getValues("users")[0]?.id ?? team?.id));
}, [formMethods.getValues("users"), hashedUrl, team?.id]);
const toggleGuests = (enabled: boolean) => {
const bookingFields = formMethods.getValues("bookingFields");
@@ -103,12 +101,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
};
}
return field;
})
}),
{ shouldDirty: true }
);
};
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
formMethods.getValues(),
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
@@ -121,8 +120,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
const closeEventNameTip = () => setShowEventNameTip(false);
const displayDestinationCalendarSelector =
!!connectedCalendarsQuery.data?.connectedCalendars.length && !team;
const setEventName = (value: string) => formMethods.setValue("eventName", value);
return (
<div className="flex flex-col space-y-4">
{/**
@@ -131,7 +131,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
* This will fallback to each user selected destination calendar.
*/}
<div className="border-subtle space-y-6 rounded-lg border p-6">
{!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && (
{displayDestinationCalendarSelector && (
<div className="flex flex-col">
<div className="flex justify-between">
<div>
@@ -145,12 +145,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
</Link>
</div>
<Controller
control={formMethods.control}
name="destinationCalendar"
defaultValue={eventType.destinationCalendar || undefined}
render={({ field: { onChange, value } }) => (
<DestinationCalendarSelector
destinationCalendar={eventType.destinationCalendar}
value={value ? value.externalId : undefined}
onChange={onChange}
hidePlaceholder
@@ -167,7 +164,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
type="text"
{...shouldLockDisableProps("eventName")}
placeholder={eventNamePlaceholder}
defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")}
addOnSuffix={
<Button
@@ -181,6 +177,27 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
}
/>
</div>
{displayDestinationCalendarSelector && (
<div className="w-full">
<Switch
tooltip={t("reconnect_calendar_to_use")}
label={
<>
{t("display_add_to_calendar_organizer")}
<InfoIcon className="text-default hover:text-attention hover:bg-attention ms-1 inline h-4 w-4 rounded-md" />
</>
}
checked={useEventTypeDestinationCalendarEmail}
onCheckedChange={(val) => {
setUseEventTypeDestinationCalendarEmail(val);
formMethods.setValue("useEventTypeDestinationCalendarEmail", val, { shouldDirty: true });
if (val) {
showToast(t("reconnect_calendar_to_use"), "warning");
}
}}
/>
</div>
)}
</div>
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} isOuterBorder={true} />
@@ -194,7 +211,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
{...shouldLockDisableProps("bookingFields")}
dataStore={{
options: {
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
locations: getLocationsOptionsForSelect(formMethods.getValues("locations") ?? [], t),
},
}}
/>
@@ -203,21 +220,20 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<RequiresConfirmationController
eventType={eventType}
seatsEnabled={seatsEnabled}
metadata={eventType.metadata}
metadata={formMethods.getValues("metadata")}
requiresConfirmation={requiresConfirmation}
onRequiresConfirmation={setRequiresConfirmation}
/>
<Controller
name="requiresBookerEmailVerification"
control={formMethods.control}
defaultValue={eventType.requiresBookerEmailVerification}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("requires_booker_email_verification")}
data-testid="requires-booker-email-verification"
{...shouldLockDisableProps("requiresBookerEmailVerification")}
description={t("description_requires_booker_email_verification")}
checked={value}
@@ -228,14 +244,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Controller
name="hideCalendarNotes"
control={formMethods.control}
defaultValue={eventType.hideCalendarNotes}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_notes")}
data-testid="disable-notes"
{...shouldLockDisableProps("hideCalendarNotes")}
description={t("disable_notes_description")}
checked={value}
@@ -246,7 +261,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Controller
name="successRedirectUrl"
control={formMethods.control}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
@@ -258,6 +272,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
)}
childrenClassName="lg:ml-0"
title={t("redirect_success_booking")}
data-testid="redirect-success-booking"
{...successRedirectUrlLocked}
description={t("redirect_url_description")}
checked={redirectUrlVisible}
@@ -272,16 +287,17 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
labelSrOnly
disabled={successRedirectUrlLocked.disabled}
placeholder={t("external_redirect_url")}
data-testid="external-redirect-url"
required={redirectUrlVisible}
type="text"
defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")}
/>
<div
className={classNames(
"p-1 text-sm text-orange-600",
formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
)}>
)}
data-testid="redirect-url-warning">
{t("redirect_url_warning")}
</div>
</div>
@@ -302,6 +318,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
title={t("enable_private_url")}
Badge={
<a
data-testid="hashedLinkCheck-info"
target="_blank"
rel="noreferrer"
href="https://cal.com/docs/core-features/event-types/single-use-private-links">
@@ -312,7 +329,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
description={t("private_link_description", { appName: APP_NAME })}
checked={hashedLinkVisible}
onCheckedChange={(e) => {
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
formMethods.setValue("hashedLink", e ? hashedUrl : undefined, { shouldDirty: true });
setHashedLinkVisible(e);
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
@@ -327,7 +344,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
hint={t("private_link_hint")}
defaultValue={placeholderHashedLink}
addOnSuffix={
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Tooltip
content={
formMethods.getValues("hashedLink") ? t("copy_to_clipboard") : t("enabled_after_update")
}>
<Button
color="minimal"
size="sm"
@@ -336,7 +356,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
aria-label="copy link"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
if (formMethods.getValues("hashedLink")) {
showToast(t("private_link_copied"), "success");
} else {
showToast(t("enabled_after_update_description"), "warning");
@@ -353,8 +373,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Controller
name="seatsPerTimeSlotEnabled"
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
@@ -375,10 +393,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
// Enabling seats will disable guests and requiring confirmation until fully supported
if (e) {
toggleGuests(false);
formMethods.setValue("requiresConfirmation", false);
formMethods.setValue("requiresConfirmation", false, { shouldDirty: true });
setRequiresConfirmation(false);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("seatsPerTimeSlot", 2);
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2, {
shouldDirty: true,
});
} else {
formMethods.setValue("seatsPerTimeSlot", null);
toggleGuests(true);
@@ -388,8 +408,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div>
<TextField
@@ -399,30 +417,40 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
label={t("number_of_seats")}
type="number"
disabled={seatsLocked.disabled}
defaultValue={value || 2}
defaultValue={value}
min={1}
containerClassName="max-w-80"
addOnSuffix={<>{t("seats")}</>}
onChange={(e) => {
onChange(Math.abs(Number(e.target.value)));
}}
data-testid="seats-per-time-slot"
/>
<div className="mt-4">
<CheckboxField
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
defaultChecked={!!eventType.seatsShowAttendees}
<Controller
name="seatsShowAttendees"
render={({ field: { value, onChange } }) => (
<CheckboxField
data-testid="show-attendees"
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => onChange(e)}
checked={value}
/>
)}
/>
</div>
<div className="mt-2">
<CheckboxField
description={t("show_available_seats_count")}
disabled={seatsLocked.disabled}
onChange={(e) =>
formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)
}
defaultChecked={!!eventType.seatsShowAvailabilityCount}
<Controller
name="seatsShowAvailabilityCount"
render={({ field: { value, onChange } }) => (
<CheckboxField
description={t("show_available_seats_count")}
disabled={seatsLocked.disabled}
onChange={(e) => onChange(e)}
checked={value}
/>
)}
/>
</div>
</div>
@@ -436,8 +464,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
/>
<Controller
name="lockTimeZoneToggleOnBookingPage"
control={formMethods.control}
defaultValue={eventType.lockTimeZoneToggleOnBookingPage}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
@@ -448,63 +474,52 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
description={t("description_lock_timezone_toggle_on_booking_page")}
checked={value}
onCheckedChange={(e) => onChange(e)}
data-testid="lock-timezone-toggle"
/>
)}
/>
{allowDisablingAttendeeConfirmationEmails(workflows) && (
<>
<Controller
name="metadata.disableStandardEmails.confirmation.attendee"
control={formMethods.control}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_attendees_confirmation_emails")}
description={t("disable_attendees_confirmation_emails_description")}
checked={value || false}
onCheckedChange={(e) => {
formMethods.setValue("metadata.disableStandardEmails.confirmation.attendee", e);
onChange(e);
}}
/>
</>
)}
/>
</>
<Controller
name="metadata.disableStandardEmails.confirmation.attendee"
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_attendees_confirmation_emails")}
description={t("disable_attendees_confirmation_emails_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
</>
)}
/>
)}
{allowDisablingHostConfirmationEmails(workflows) && (
<>
<Controller
name="metadata.disableStandardEmails.confirmation.host"
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_host_confirmation_emails")}
description={t("disable_host_confirmation_emails_description")}
checked={value || false}
onCheckedChange={(e) => {
formMethods.setValue("metadata.disableStandardEmails.confirmation.host", e);
onChange(e);
}}
/>
</>
)}
/>
</>
<Controller
name="metadata.disableStandardEmails.confirmation.host"
defaultValue={!!formMethods.getValues("seatsPerTimeSlot")}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_host_confirmation_emails")}
description={t("disable_host_confirmation_emails_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
</>
)}
/>
)}
{showEventNameTip && (
<CustomEventTypeModal
close={closeEventNameTip}
setValue={setEventName}
defaultValue={formMethods.getValues("eventName") || eventType.eventName || ""}
setValue={(val: string) => formMethods.setValue("eventName", val, { shouldDirty: true })}
defaultValue={formMethods.getValues("eventName")}
placeHolder={eventNamePlaceholder}
event={eventNameObject}
/>
+15 -12
View File
@@ -18,23 +18,27 @@ export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
const { t } = useLocale();
const { data: eventTypeApps, isLoading } = trpc.viewer.integrations.useQuery({
const { data: eventTypeApps, isPending } = trpc.viewer.integrations.useQuery({
extendsFeature: "EventType",
teamId: eventType.team?.id || eventType.parent?.teamId,
});
const methods = useFormContext<FormValues>();
const formMethods = useFormContext<FormValues>();
const installedApps =
eventTypeApps?.items.filter((app) => app.userCredentialIds.length || app.teams.length) || [];
const notInstalledApps =
eventTypeApps?.items.filter((app) => !app.userCredentialIds.length && !app.teams.length) || [];
const allAppsData = methods.watch("metadata")?.apps || {};
const allAppsData = formMethods.watch("metadata")?.apps || {};
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
methods.setValue("metadata", {
...methods.getValues("metadata"),
apps: _allAppsData,
});
formMethods.setValue(
"metadata",
{
...formMethods.getValues("metadata"),
apps: _allAppsData,
},
{ shouldDirty: true }
);
};
const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => {
@@ -47,7 +51,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
};
};
const eventTypeFormMetadata = methods.getValues("metadata");
const eventTypeFormMetadata = formMethods.getValues("metadata");
const getAppDataSetter = (
appId: EventTypeAppsList,
@@ -56,8 +60,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
): SetAppData => {
return function (key, value) {
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
const allAppsDataFromForm = formMethods.getValues("metadata")?.apps || {};
const appData = allAppsDataFromForm[appId];
setAllAppsData({
...allAppsDataFromForm,
@@ -138,7 +141,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
message={t("locked_apps_description")}
/>
)}
{!isLoading && !installedApps?.length ? (
{!isPending && !installedApps?.length ? (
<EmptyScreen
Icon={Grid}
headline={t("empty_installed_apps_headline")}
@@ -179,7 +182,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
</div>
{!shouldLockDisableProps("apps").disabled && (
<div className="bg-muted mt-6 rounded-md p-8">
{!isLoading && notInstalledApps?.length ? (
{!isPending && notInstalledApps?.length ? (
<>
<h2 className="text-emphasis mb-2 text-xl font-semibold leading-5 tracking-[0.01em]">
{t("available_apps_lower_case")}
@@ -85,7 +85,7 @@ const EventTypeScheduleDetails = memo(
const { watch } = useFormContext<FormValues>();
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
const { isPending, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{
scheduleId:
scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
@@ -112,7 +112,7 @@ const EventTypeScheduleDetails = memo(
)}>
{day}
</span>
{isLoading ? (
{isPending ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
@@ -142,7 +142,7 @@ const EventTypeScheduleDetails = memo(
{!!schedule?.id && !schedule.isManaged && !schedule.readOnly && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
disabled={isPending}
color="minimal"
EndIcon={ExternalLink}
target="_blank"
@@ -169,8 +169,15 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
const watchSchedule = watch("schedule");
const [options, setOptions] = useState<AvailabilityOption[]>([]);
const { isLoading } = trpc.viewer.availability.list.useQuery(undefined, {
onSuccess: ({ schedules }) => {
const { data, isPending } = trpc.viewer.availability.list.useQuery(undefined);
useEffect(
function refactorMeWithoutEffect() {
if (!data) {
return;
}
const schedules = data.schedules;
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
@@ -223,15 +230,15 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
);
setValue("availability", value);
setValue("availability", value, { shouldDirty: true });
},
});
[data]
);
const availabilityValue = watch("availability");
useEffect(() => {
if (!availabilityValue?.value) return;
setValue("schedule", availabilityValue.value);
setValue("schedule", availabilityValue.value, { shouldDirty: true });
}, [availabilityValue, setValue]);
return (
@@ -241,8 +248,8 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
{t("availability")}
{shouldLockIndicator("availability")}
</label>
{isLoading && <SelectSkeletonLoader />}
{!isLoading && (
{isPending && <SelectSkeletonLoader />}
{!isPending && (
<Controller
name="schedule"
render={({ field }) => {
@@ -253,7 +260,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
isSearchable={false}
onChange={(selected) => {
field.onChange(selected?.value || null);
if (selected?.value) setValue("availability", selected);
if (selected?.value) setValue("availability", selected, { shouldDirty: true });
}}
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
value={availabilityValue}
@@ -291,7 +298,7 @@ const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSe
onCheckedChange={(checked) => {
onChange(!checked);
if (!checked) {
setValue("schedule", null);
setValue("schedule", null, { shouldDirty: true });
}
}}
title={t("choose_common_schedule_team_event")}
@@ -1,10 +1,10 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import type { FormValues } from "pages/event-types/[type]";
import type { Key } from "react";
import React, { useEffect, useState } from "react";
import type { UseFormRegisterReturn } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { Controller, useFormContext } from "react-hook-form";
import type { SingleValue } from "react-select";
import { classNames } from "@calcom/lib";
@@ -61,7 +61,8 @@ const MinimumBookingNoticeInput = React.forwardRef<
minimumBookingNoticeDisplayValues.type,
"minutes",
minimumBookingNoticeDisplayValues.value
)
),
{ shouldDirty: true }
);
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
@@ -107,7 +108,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
);
});
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
export const EventLimitsTab = () => {
const { t, i18n } = useLocale();
const formMethods = useFormContext<FormValues>();
@@ -126,47 +127,29 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
},
];
const periodType =
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
PERIOD_TYPES.find((s) => s.type === "UNLIMITED");
const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
startDate: new Date(eventType.periodStartDate || Date.now()),
endDate: new Date(eventType.periodEndDate || Date.now()),
});
const watchPeriodType = useWatch({
control: formMethods.control,
name: "periodType",
defaultValue: periodType?.type,
});
const watchPeriodType = formMethods.watch("periodType");
const optionsPeriod = [
{ value: 1, label: t("calendar_days") },
{ value: 0, label: t("business_days") },
{ value: 1, label: t("calendar_days") },
];
// offsetStart toggle is client-side only, opened by default if offsetStart is set
const offsetStartValue = useWatch({
control: formMethods.control,
name: "offsetStart",
});
const [offsetToggle, setOffsetToggle] = useState(() => offsetStartValue > 0);
const [offsetToggle, setOffsetToggle] = useState(formMethods.getValues("offsetStart") > 0);
// Preview how the offset will affect start times
const watchOffsetStartValue = formMethods.watch("offsetStart");
const offsetOriginalTime = new Date();
offsetOriginalTime.setHours(9, 0, 0, 0);
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + offsetStartValue * 60 * 1000);
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + watchOffsetStartValue * 60 * 1000);
return (
<div>
<div className="border-subtle space-y-6 rounded-lg border p-6">
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
<div className="w-full">
<Label htmlFor="beforeBufferTime">{t("before_event")}</Label>
<Label htmlFor="beforeEventBuffer">{t("before_event")}</Label>
<Controller
name="beforeBufferTime"
control={formMethods.control}
defaultValue={eventType.beforeEventBuffer || 0}
name="beforeEventBuffer"
render={({ field: { onChange, value } }) => {
const beforeBufferOptions = [
{
@@ -194,11 +177,9 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
</div>
<div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")}</Label>
<Label htmlFor="afterEventBuffer">{t("after_event")}</Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
defaultValue={eventType.afterEventBuffer || 0}
name="afterEventBuffer"
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
@@ -235,7 +216,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<Label htmlFor="slotInterval">{t("slot_interval")}</Label>
<Controller
name="slotInterval"
control={formMethods.control}
render={() => {
const slotIntervalOptions = [
{
@@ -251,11 +231,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<Select
isSearchable={false}
onChange={(val) => {
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null, {
shouldDirty: true,
});
}}
defaultValue={
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
slotIntervalOptions[0]
slotIntervalOptions.find(
(option) => option.value === formMethods.getValues("slotInterval")
) || slotIntervalOptions[0]
}
options={slotIntervalOptions}
/>
@@ -267,7 +250,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</div>
<Controller
name="bookingLimits"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = Object.keys(value ?? {}).length > 0;
return (
@@ -279,11 +261,15 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
checked={isChecked}
onCheckedChange={(active) => {
if (active) {
formMethods.setValue("bookingLimits", {
PER_DAY: 1,
});
formMethods.setValue(
"bookingLimits",
{
PER_DAY: 1,
},
{ shouldDirty: true }
);
} else {
formMethods.setValue("bookingLimits", {});
formMethods.setValue("bookingLimits", {}, { shouldDirty: true });
}
}}
switchContainerClassName={classNames(
@@ -300,8 +286,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
<Controller
name="onlyShowFirstAvailableSlot"
control={formMethods.control}
render={({ field: { value } }) => {
render={({ field: { onChange, value } }) => {
const isChecked = value;
return (
<SettingsToggle
@@ -311,7 +296,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
description={t("limit_booking_only_first_slot_description")}
checked={isChecked}
onCheckedChange={(active) => {
formMethods.setValue("onlyShowFirstAvailableSlot", active ?? false);
onChange(active ?? false);
}}
switchContainerClassName={classNames(
"border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
@@ -323,8 +308,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
<Controller
name="durationLimits"
control={formMethods.control}
render={({ field: { value } }) => {
render={({ field: { onChange, value } }) => {
const isChecked = Object.keys(value ?? {}).length > 0;
return (
<SettingsToggle
@@ -340,11 +324,11 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
checked={isChecked}
onCheckedChange={(active) => {
if (active) {
formMethods.setValue("durationLimits", {
onChange({
PER_DAY: 60,
});
} else {
formMethods.setValue("durationLimits", {});
onChange({});
}
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
@@ -361,8 +345,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
<Controller
name="periodType"
control={formMethods.control}
render={({ field: { value } }) => {
render={({ field: { onChange, value } }) => {
const isChecked = value && value !== "UNLIMITED";
return (
@@ -377,12 +360,16 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
title={t("limit_future_bookings")}
description={t("limit_future_bookings_description")}
checked={isChecked}
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
onCheckedChange={(bool) =>
// formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED", { shouldDirty: true })
onChange(bool ? "ROLLING" : "UNLIMITED")
}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<RadioGroup.Root
defaultValue={watchPeriodType}
value={watchPeriodType}
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
onValueChange={(val) =>
formMethods.setValue("periodType", val as PeriodType, { shouldDirty: true })
}>
{PERIOD_TYPES.map((period) => {
if (period.type === "UNLIMITED") return null;
return (
@@ -408,22 +395,27 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
placeholder="30"
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
/>
<Select
options={optionsPeriod}
isSearchable={false}
onChange={(opt) => {
onChange={(opt) =>
formMethods.setValue(
"periodCountCalendarDays",
opt?.value.toString() as "0" | "1"
);
}}
defaultValue={
optionsPeriod.find(
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
) ?? optionsPeriod[0]
opt?.value === 1 ? true : false,
{ shouldDirty: true }
)
}
name="periodCoundCalendarDays"
value={optionsPeriod.find((opt) => {
opt.value ===
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0);
})}
defaultValue={optionsPeriod.find(
(opt) =>
opt.value ===
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0)
)}
/>
</div>
)}
@@ -431,14 +423,12 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
<Controller
name="periodDates"
control={formMethods.control}
defaultValue={periodDates}
render={() => (
render={({ field: { onChange } }) => (
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
endDate={formMethods.getValues("periodDates").endDate}
onDatesChange={({ startDate, endDate }) => {
formMethods.setValue("periodDates", {
onChange({
startDate,
endDate,
});
@@ -472,7 +462,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
onCheckedChange={(active) => {
setOffsetToggle(active);
if (!active) {
formMethods.setValue("offsetStart", 0);
formMethods.setValue("offsetStart", 0, { shouldDirty: true });
}
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
@@ -481,7 +471,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
type="number"
containerClassName="max-w-80"
label={t("offset_start")}
{...formMethods.register("offsetStart")}
{...formMethods.register("offsetStart", { setValueAs: (value) => Number(value) })}
addOnSuffix={<>{t("minutes")}</>}
hint={t("offset_start_description", {
originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
@@ -603,11 +593,16 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
);
if (!rest || !currentKeys.length) return;
//currentDurationLimits is always defined so can be casted
// @ts-expect-error FIXME Fix these typings
setValue(propertyName, {
...watchIntervalLimits,
[rest.value]: defaultLimit,
});
setValue(
propertyName,
// @ts-expect-error FIXME Fix these typings
{
...watchIntervalLimits,
[rest.value]: defaultLimit,
},
{ shouldDirty: true }
);
};
return (
@@ -637,7 +632,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
)}
onLimitChange={(intervalLimitKey, val) =>
// @ts-expect-error FIXME Fix these typings
setValue(`${propertyName}.${intervalLimitKey}`, val)
setValue(`${propertyName}.${intervalLimitKey}`, val, { shouldDirty: true })
}
onDelete={(intervalLimitKey) => {
const current = currentIntervalLimits;
@@ -9,9 +9,5 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
const requirePayment = paymentAppData.price > 0;
return (
<div className="">
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
</div>
);
return <RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />;
};
+40 -54
View File
@@ -11,7 +11,7 @@ import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { CAL_URL } from "@calcom/lib/constants";
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import { slugify } from "@calcom/lib/slugify";
@@ -48,21 +48,19 @@ const getLocationFromType = (
}
};
const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
const getLocationInfo = ({
eventType,
locationOptions,
}: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
const locationAvailable =
props.eventType.locations &&
props.eventType.locations.length > 0 &&
props.locationOptions.some((op) =>
op.options.find((opt) => opt.value === props.eventType.locations[0].type)
);
const locationDetails = props.eventType.locations &&
props.eventType.locations.length > 0 &&
eventType.locations &&
eventType.locations.length > 0 &&
locationOptions.some((op) => op.options.find((opt) => opt.value === eventType.locations[0].type));
const locationDetails = eventType.locations &&
eventType.locations.length > 0 &&
!locationAvailable && {
slug: props.eventType.locations[0].type
.replace("integrations:", "")
.replace(":", "-")
.replace("_video", ""),
name: props.eventType.locations[0].type
slug: eventType.locations[0].type.replace("integrations:", "").replace(":", "-").replace("_video", ""),
name: eventType.locations[0].type
.replace("integrations:", "")
.replace(":", " ")
.replace("_video", "")
@@ -72,16 +70,11 @@ const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locatio
};
return { locationAvailable, locationDetails };
};
interface DescriptionEditorProps {
description?: string | null;
editable?: boolean;
}
const DescriptionEditor = (props: DescriptionEditorProps) => {
const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
const formMethods = useFormContext<FormValues>();
const [mounted, setIsMounted] = useState(false);
const { t } = useLocale();
const { description } = props;
const [firstRender, setFirstRender] = useState(true);
useEffect(() => {
setIsMounted(true);
@@ -89,11 +82,11 @@ const DescriptionEditor = (props: DescriptionEditorProps) => {
return mounted ? (
<Editor
getText={() => md.render(formMethods.getValues("description") || description || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
getText={() => md.render(formMethods.getValues("description") || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value), { shouldDirty: true })}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
editable={props.editable}
editable={isEditable}
firstRender={firstRender}
setFirstRender={setFirstRender}
/>
@@ -113,7 +106,9 @@ export const EventSetupTab = (
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const { eventType, team, destinationCalendar } = props;
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
const [multipleDuration, setMultipleDuration] = useState(
formMethods.getValues("metadata")?.multipleDuration
);
const orgBranding = useOrgBranding();
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
@@ -143,12 +138,12 @@ export const EventSetupTab = (
}>
>(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
const [defaultDuration, setDefaultDuration] = useState(
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
selectedMultipleDuration.find((opt) => opt.value === formMethods.getValues("length")) ?? null
);
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
useLockedFieldsManager(
eventType,
formMethods.getValues(),
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
@@ -195,7 +190,6 @@ export const EventSetupTab = (
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
control={formMethods.control}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
@@ -219,7 +213,6 @@ export const EventSetupTab = (
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
control={formMethods.control}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
@@ -339,7 +332,7 @@ export const EventSetupTab = (
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
onChange={(e) => {
const fieldValues = formMethods.getValues().locations[index];
const fieldValues = formMethods.getValues("locations")[index];
updateLocationField(index, {
...fieldValues,
displayLocationPublicly: e.target.checked,
@@ -408,7 +401,7 @@ export const EventSetupTab = (
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
Change it{" "}
<Link
href={`${CAL_URL}/event-types/${eventType.id}?tabName=advanced`}
href={`${WEBAPP_URL}/event-types/${formMethods.getValues("id")}?tabName=advanced`}
className="underline">
here.
</Link>{" "}
@@ -419,7 +412,7 @@ export const EventSetupTab = (
{isChildrenManagedEventType && !locationAvailable && locationDetails && (
<p className="pl-1 text-sm leading-none text-red-600">
{t("app_not_connected", { appName: locationDetails.name })}{" "}
<a className="underline" href={`${CAL_URL}/apps/${locationDetails.slug}`}>
<a className="underline" href={`${WEBAPP_URL}/apps/${locationDetails.slug}`}>
{t("connect_now")}
</a>
</p>
@@ -451,9 +444,10 @@ export const EventSetupTab = (
const lengthLockedProps = shouldLockDisableProps("length");
const descriptionLockedProps = shouldLockDisableProps("description");
const urlPrefix = orgBranding
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
: `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
: `${WEBSITE_URL?.replace(/^(https?:|)\/\//, "")}`;
return (
<div>
@@ -463,7 +457,6 @@ export const EventSetupTab = (
required
label={t("title")}
{...shouldLockDisableProps("title")}
defaultValue={eventType.title}
{...formMethods.register("title")}
/>
<div>
@@ -471,23 +464,19 @@ export const EventSetupTab = (
{t("description")}
{shouldLockIndicator("description")}
</Label>
<DescriptionEditor
description={eventType?.description}
editable={!descriptionLockedProps.disabled}
/>
<DescriptionEditor isEditable={!descriptionLockedProps.disabled} />
</div>
<TextField
required
label={t("URL")}
{...shouldLockDisableProps("slug")}
defaultValue={eventType.slug}
addOnLeading={
<>
{urlPrefix}/
{!isManagedEventType
? team
? (orgBranding ? "" : "team/") + team.slug
: eventType.users[0].username
: formMethods.getValues("users")[0].username
: t("username_placeholder")}
/
</>
@@ -523,16 +512,16 @@ export const EventSetupTab = (
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
if (newOptions.length > 0) {
setDefaultDuration(newOptions[0]);
formMethods.setValue("length", newOptions[0].value);
formMethods.setValue("length", newOptions[0].value, { shouldDirty: true });
} else {
setDefaultDuration(null);
}
}
if (newOptions.length === 1 && defaultDuration === null) {
setDefaultDuration(newOptions[0]);
formMethods.setValue("length", newOptions[0].value);
formMethods.setValue("length", newOptions[0].value, { shouldDirty: true });
}
formMethods.setValue("metadata.multipleDuration", values);
formMethods.setValue("metadata.multipleDuration", values, { shouldDirty: true });
}}
/>
</div>
@@ -553,7 +542,7 @@ export const EventSetupTab = (
setDefaultDuration(
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
);
if (option) formMethods.setValue("length", option.value);
if (option) formMethods.setValue("length", option.value, { shouldDirty: true });
}}
/>
</div>
@@ -564,7 +553,7 @@ export const EventSetupTab = (
type="number"
{...lengthLockedProps}
label={t("duration")}
defaultValue={eventType.length ?? 15}
defaultValue={formMethods.getValues("length") ?? 15}
{...formMethods.register("length")}
addOnSuffix={<>{t("minutes")}</>}
min={1}
@@ -580,12 +569,14 @@ export const EventSetupTab = (
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
setSelectedMultipleDuration([]);
setDefaultDuration(null);
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
formMethods.setValue("length", eventType.length, { shouldDirty: true });
} else {
setMultipleDuration([]);
formMethods.setValue("metadata.multipleDuration", []);
formMethods.setValue("length", 0);
formMethods.setValue("metadata.multipleDuration", [], { shouldDirty: true });
formMethods.setValue("length", 0, { shouldDirty: true });
}
}}
/>
@@ -600,12 +591,7 @@ export const EventSetupTab = (
{shouldLockIndicator("locations")}
</Skeleton>
<Controller
name="locations"
control={formMethods.control}
defaultValue={eventType.locations || []}
render={() => <Locations />}
/>
<Controller name="locations" render={() => <Locations />} />
</div>
</div>
</div>
+360 -96
View File
@@ -1,8 +1,8 @@
import { Trans } from "next-i18next";
import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef } from "react";
import type { ComponentProps } from "react";
import type { EventTypeSetupProps, FormValues, Host } from "pages/event-types/[type]";
import { useEffect, useRef, useState } from "react";
import type { ComponentProps, Dispatch, SetStateAction } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { Options } from "react-select";
@@ -11,7 +11,7 @@ import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTea
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { Label, Select } from "@calcom/ui";
import { Label, Select, SettingsToggle } from "@calcom/ui";
interface IUserToValue {
id: number | null;
@@ -21,6 +21,13 @@ interface IUserToValue {
email: string;
}
type TeamMember = {
value: string;
label: string;
avatar: string;
email: string;
};
const mapUserToValue = ({ id, name, username, avatar, email }: IUserToValue, pendingString: string) => ({
value: `${id || ""}`,
label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`,
@@ -97,6 +104,43 @@ const ChildrenEventTypesList = ({
);
};
const AssignAllTeamMembers = ({
assignAllTeamMembers,
setAssignAllTeamMembers,
onActive,
onInactive,
}: {
assignAllTeamMembers: boolean;
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
onActive: () => void;
onInactive?: () => void;
}) => {
const { t } = useLocale();
const { setValue } = useFormContext<FormValues>();
return (
<Controller<FormValues>
name="assignAllTeamMembers"
render={() => (
<SettingsToggle
title={t("automatically_add_all_team_members")}
labelClassName="mt-0.5 font-normal"
checked={assignAllTeamMembers}
onCheckedChange={(active) => {
setValue("assignAllTeamMembers", active, { shouldDirty: true });
setAssignAllTeamMembers(active);
if (active) {
onActive();
} else if (!!onInactive) {
onInactive();
}
}}
/>
)}
/>
);
};
const CheckedHostField = ({
labelText,
placeholder,
@@ -107,18 +151,18 @@ const CheckedHostField = ({
helperText,
...rest
}: {
labelText: string;
labelText?: string;
placeholder: string;
isFixed: boolean;
value: { isFixed: boolean; userId: number }[];
onChange?: (options: { isFixed: boolean; userId: number }[]) => void;
value: Host[];
onChange?: (options: Host[]) => void;
options?: Options<CheckedSelectOption>;
helperText?: React.ReactNode | string;
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
return (
<div className="bg-muted flex flex-col space-y-5 p-4">
<div className="flex flex-col rounded-md">
<div>
<Label>{labelText}</Label>
{labelText ? <Label>{labelText}</Label> : <></>}
<CheckedTeamSelect
isOptionDisabled={(option) => !!value.find((host) => host.userId.toString() === option.value)}
onChange={(options) => {
@@ -127,23 +171,22 @@ const CheckedHostField = ({
options.map((option) => ({
isFixed,
userId: parseInt(option.value, 10),
priority: option.priority ?? 2,
}))
);
}}
value={(value || [])
.filter(({ isFixed: _isFixed }) => isFixed === _isFixed)
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.find((member) => member.value === host.userId.toString())!
)
.map((host) => {
const option = options.find((member) => member.value === host.userId.toString());
return option ? { ...option, priority: host.priority ?? 2, isFixed } : options[0];
})
.filter(Boolean)}
controlShouldRenderValue={false}
options={options}
placeholder={placeholder}
{...rest}
/>
{helperText && <p className="text-subtle text-sm">{helperText}</p>}
</div>
</div>
);
@@ -161,76 +204,255 @@ const FixedHostHelper = (
</Trans>
);
const FixedHosts = ({
teamMembers,
value,
onChange,
assignAllTeamMembers,
setAssignAllTeamMembers,
isRoundRobinEvent = false,
}: {
value: Host[];
onChange: (hosts: Host[]) => void;
teamMembers: TeamMember[];
assignAllTeamMembers: boolean;
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
isRoundRobinEvent?: boolean;
}) => {
const { t } = useLocale();
const { getValues, setValue } = useFormContext<FormValues>();
const hasActiveFixedHosts = isRoundRobinEvent && getValues("hosts").some((host) => host.isFixed);
const [isDisabled, setIsDisabled] = useState(hasActiveFixedHosts);
return (
<div className="mt-5 rounded-lg">
{!isRoundRobinEvent ? (
<>
<div className="border-subtle mt-5 rounded-t-md border p-6 pb-5">
<Label className="mb-1 text-sm font-semibold">{t("fixed_hosts")}</Label>
<p className="text-subtle max-w-full break-words text-sm leading-tight">{FixedHostHelper}</p>
</div>
<div className="border-subtle rounded-b-md border border-t-0">
<AddMembersWithSwitch
teamMembers={teamMembers}
value={value}
onChange={onChange}
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
automaticAddAllEnabled={!isRoundRobinEvent}
isFixed={true}
onActive={() =>
setValue(
"hosts",
teamMembers.map((teamMember) => ({
isFixed: true,
userId: parseInt(teamMember.value, 10),
priority: 2,
})),
{ shouldDirty: true }
)
}
/>
</div>
</>
) : (
<SettingsToggle
toggleSwitchAtTheEnd={true}
title={t("fixed_hosts")}
description={FixedHostHelper}
checked={isDisabled}
labelClassName="text-sm"
descriptionClassName=" text-sm text-subtle"
onCheckedChange={(checked) => {
if (!checked) {
const rrHosts = getValues("hosts")
.filter((host) => !host.isFixed)
.sort((a, b) => (b.priority ?? 2) - (a.priority ?? 2));
setValue("hosts", rrHosts, { shouldDirty: true });
}
setIsDisabled(checked);
}}
childrenClassName="lg:ml-0">
<div className="border-subtle flex flex-col gap-6 rounded-bl-md rounded-br-md border border-t-0">
<AddMembersWithSwitch
teamMembers={teamMembers}
value={value}
onChange={onChange}
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
automaticAddAllEnabled={!isRoundRobinEvent}
isFixed={true}
onActive={() =>
setValue(
"hosts",
teamMembers.map((teamMember) => ({
isFixed: true,
userId: parseInt(teamMember.value, 10),
priority: 2,
})),
{ shouldDirty: true }
)
}
/>
</div>
</SettingsToggle>
)}
</div>
);
};
const AddMembersWithSwitch = ({
teamMembers,
value,
onChange,
assignAllTeamMembers,
setAssignAllTeamMembers,
automaticAddAllEnabled,
onActive,
isFixed,
}: {
value: Host[];
onChange: (hosts: Host[]) => void;
teamMembers: TeamMember[];
assignAllTeamMembers: boolean;
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
automaticAddAllEnabled: boolean;
onActive: () => void;
isFixed: boolean;
}) => {
const { t } = useLocale();
const { setValue } = useFormContext<FormValues>();
return (
<div className="rounded-md ">
<div className="flex flex-col rounded-md px-6 pb-2 pt-6">
{automaticAddAllEnabled ? (
<div className="mb-2">
<AssignAllTeamMembers
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
onActive={onActive}
onInactive={() => setValue("hosts", [], { shouldDirty: true })}
/>
</div>
) : (
<></>
)}
{!assignAllTeamMembers || !automaticAddAllEnabled ? (
<CheckedHostField
value={value}
onChange={onChange}
isFixed={isFixed}
options={teamMembers.sort(sortByLabel)}
placeholder={t("add_attendees")}
/>
) : (
<></>
)}
</div>
</div>
);
};
const RoundRobinHosts = ({
teamMembers,
value,
onChange,
assignAllTeamMembers,
setAssignAllTeamMembers,
}: {
value: { isFixed: boolean; userId: number }[];
onChange: (hosts: { isFixed: boolean; userId: number }[]) => void;
teamMembers: {
value: string;
label: string;
avatar: string;
email: string;
}[];
value: Host[];
onChange: (hosts: Host[]) => void;
teamMembers: TeamMember[];
assignAllTeamMembers: boolean;
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
}) => {
const { t } = useLocale();
const { setValue } = useFormContext<FormValues>();
return (
<>
<CheckedHostField
options={teamMembers.sort(sortByLabel)}
isFixed={true}
onChange={(changeValue) => {
onChange([...value.filter(({ isFixed }) => !isFixed), ...changeValue]);
}}
value={value}
placeholder={t("add_fixed_hosts")}
labelText={t("fixed_hosts")}
helperText={FixedHostHelper}
/>
<CheckedHostField
options={teamMembers.sort(sortByLabel)}
onChange={(changeValue) => onChange([...value.filter(({ isFixed }) => isFixed), ...changeValue])}
value={value}
isFixed={false}
placeholder={t("add_attendees")}
labelText={t("round_robin_hosts")}
helperText={t("round_robin_helper")}
/>
</>
<div className="rounded-lg ">
<div className="border-subtle mt-5 rounded-t-md border p-6 pb-5">
<Label className="mb-1 text-sm font-semibold">{t("round_robin_hosts")}</Label>
<p className="text-subtle max-w-full break-words text-sm leading-tight">{t("round_robin_helper")}</p>
</div>
<div className="border-subtle rounded-b-md border border-t-0">
<AddMembersWithSwitch
teamMembers={teamMembers}
value={value}
onChange={onChange}
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
automaticAddAllEnabled={true}
isFixed={false}
onActive={() =>
setValue(
"hosts",
teamMembers
.map((teamMember) => ({
isFixed: false,
userId: parseInt(teamMember.value, 10),
priority: 2,
}))
.sort((a, b) => b.priority - a.priority),
{ shouldDirty: true }
)
}
/>
</div>
</div>
);
};
const ChildrenEventTypes = ({
childrenEventTypeOptions,
assignAllTeamMembers,
setAssignAllTeamMembers,
}: {
childrenEventTypeOptions: ReturnType<typeof mapMemberToChildrenOption>[];
assignAllTeamMembers: boolean;
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
}) => {
const { setValue } = useFormContext<FormValues>();
return (
<Controller<FormValues>
name="children"
render={({ field: { onChange, value } }) => (
<ChildrenEventTypesList value={value} options={childrenEventTypeOptions} onChange={onChange} />
)}
/>
<div className="border-subtle mt-6 space-y-5 rounded-lg border px-4 py-6 sm:px-6">
<div className="flex flex-col gap-4">
<AssignAllTeamMembers
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
onActive={() => setValue("children", childrenEventTypeOptions, { shouldDirty: true })}
/>
{!assignAllTeamMembers ? (
<Controller<FormValues>
name="children"
render={({ field: { onChange, value } }) => (
<ChildrenEventTypesList value={value} options={childrenEventTypeOptions} onChange={onChange} />
)}
/>
) : (
<></>
)}
</div>
</div>
);
};
const Hosts = ({
teamMembers,
assignAllTeamMembers,
setAssignAllTeamMembers,
}: {
teamMembers: {
value: string;
label: string;
avatar: string;
email: string;
}[];
teamMembers: TeamMember[];
assignAllTeamMembers: boolean;
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
}) => {
const { t } = useLocale();
const {
control,
resetField,
setValue,
getValues,
formState: { submitCount },
} = useFormContext<FormValues>();
@@ -250,10 +472,12 @@ const Hosts = ({
initialValue.current = { hosts: getValues("hosts"), schedulingType, submitCount };
return;
}
resetField("hosts", {
defaultValue: initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
});
}, [schedulingType, resetField, getValues, submitCount]);
setValue(
"hosts",
initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
{ shouldDirty: true }
);
}, [schedulingType, setValue, getValues, submitCount]);
return (
<Controller<FormValues>
@@ -261,26 +485,39 @@ const Hosts = ({
render={({ field: { onChange, value } }) => {
const schedulingTypeRender = {
COLLECTIVE: (
<CheckedHostField
<FixedHosts
teamMembers={teamMembers}
value={value}
onChange={onChange}
isFixed={true}
options={teamMembers.sort(sortByLabel)}
placeholder={t("add_attendees")}
labelText={t("team")}
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
/>
),
ROUND_ROBIN: (
<>
<RoundRobinHosts teamMembers={teamMembers} onChange={onChange} value={value} />
{/*<TextField
required
type="number"
label={t("minimum_round_robin_hosts_count")}
defaultValue={1}
{...formMethods.register("minimumHostCount")}
addOnSuffix={<>{t("hosts")}</>}
/>*/}
<FixedHosts
teamMembers={teamMembers}
value={value}
onChange={(changeValue) => {
onChange([...value.filter((host: Host) => !host.isFixed), ...changeValue]);
}}
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
isRoundRobinEvent={true}
/>
<RoundRobinHosts
teamMembers={teamMembers}
value={value}
onChange={(changeValue) => {
onChange(
[...value.filter((host: Host) => host.isFixed), ...changeValue].sort(
(a, b) => b.priority - a.priority
)
);
}}
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
/>
</>
),
MANAGED: <></>,
@@ -320,34 +557,61 @@ export const EventTeamTab = ({
.filter(pendingMembers)
.map((member) => mapUserToValue(member, t("pending")));
const childrenEventTypeOptions = teamMembers.filter(pendingMembers).map((member) => {
return mapMemberToChildrenOption(member, eventType.slug, t("pending"));
return mapMemberToChildrenOption(
{ ...member, eventTypes: member.eventTypes.filter((et) => et !== eventType.slug) },
eventType.slug,
t("pending")
);
});
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
const { getValues, setValue } = useFormContext<FormValues>();
const [assignAllTeamMembers, setAssignAllTeamMembers] = useState<boolean>(
getValues("assignAllTeamMembers") ?? false
);
return (
<div>
{team && !isManagedEventType && (
<div className="space-y-5">
<div className="flex flex-col">
<Label>{t("scheduling_type")}</Label>
<Controller<FormValues>
name="schedulingType"
render={({ field: { value, onChange } }) => (
<Select
options={schedulingTypeOptions}
value={schedulingTypeOptions.find((opt) => opt.value === value)}
className="w-full"
onChange={(val) => {
onChange(val?.value);
}}
/>
)}
/>
<>
<div className="border-subtle flex flex-col rounded-md">
<div className="border-subtle rounded-t-md border p-6 pb-5">
<Label className="mb-1 text-sm font-semibold">{t("assignment")}</Label>
<p className="text-subtle max-w-full break-words text-sm leading-tight">
{t("assignment_description")}
</p>
</div>
<div className="border-subtle rounded-b-md border border-t-0 p-6">
<Label>{t("scheduling_type")}</Label>
<Controller<FormValues>
name="schedulingType"
render={({ field: { value, onChange } }) => (
<Select
options={schedulingTypeOptions}
value={schedulingTypeOptions.find((opt) => opt.value === value)}
className="w-full"
onChange={(val) => {
onChange(val?.value);
setValue("assignAllTeamMembers", false, { shouldDirty: true });
setAssignAllTeamMembers(false);
}}
/>
)}
/>
</div>
</div>
<Hosts teamMembers={teamMembersOptions} />
</div>
<Hosts
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
teamMembers={teamMembersOptions}
/>
</>
)}
{team && isManagedEventType && (
<ChildrenEventTypes childrenEventTypeOptions={childrenEventTypeOptions} />
<ChildrenEventTypes
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
childrenEventTypeOptions={childrenEventTypeOptions}
/>
)}
</div>
);
@@ -14,6 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { SchedulingType } from "@calcom/prisma/enums";
import { trpc, TRPCClientError } from "@calcom/trpc/react";
import type { DialogProps } from "@calcom/ui";
import {
Button,
ButtonGroup,
@@ -67,61 +68,126 @@ type Props = {
availability?: AvailabilityOption;
isUserOrganizationAdmin: boolean;
bookerUrl: string;
activeWebhooksNumber: number;
};
function getNavigation(props: {
type getNavigationProps = {
t: TFunction;
eventType: Props["eventType"];
length: number;
id: number;
multipleDuration?: EventTypeSetupProps["eventType"]["metadata"]["multipleDuration"];
enabledAppsNumber: number;
enabledWorkflowsNumber: number;
installedAppsNumber: number;
availability: AvailabilityOption | undefined;
}) {
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
const duration =
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
};
function getNavigation({
length,
id,
multipleDuration,
t,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
}: getNavigationProps) {
const duration = multipleDuration?.map((duration) => ` ${duration}`) || length;
return [
{
name: "event_setup_tab_title",
href: `/event-types/${eventType.id}?tabName=setup`,
href: `/event-types/${id}?tabName=setup`,
icon: LinkIcon,
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
},
{
name: "event_limit_tab_title",
href: `/event-types/${eventType.id}?tabName=limits`,
href: `/event-types/${id}?tabName=limits`,
icon: Clock,
info: `event_limit_tab_description`,
},
{
name: "event_advanced_tab_title",
href: `/event-types/${eventType.id}?tabName=advanced`,
href: `/event-types/${id}?tabName=advanced`,
icon: Sliders,
info: `event_advanced_tab_description`,
},
{
name: "recurring",
href: `/event-types/${eventType.id}?tabName=recurring`,
href: `/event-types/${id}?tabName=recurring`,
icon: Repeat,
info: `recurring_event_tab_description`,
},
{
name: "apps",
href: `/event-types/${eventType.id}?tabName=apps`,
href: `/event-types/${id}?tabName=apps`,
icon: Grid,
//TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
},
{
name: "workflows",
href: `/event-types/${eventType.id}?tabName=workflows`,
href: `/event-types/${id}?tabName=workflows`,
icon: Zap,
info: `${enabledWorkflowsNumber} ${t("active")}`,
},
];
}
function DeleteDialog({
isManagedEvent,
eventTypeId,
open,
onOpenChange,
}: { isManagedEvent: string; eventTypeId: number } & Pick<DialogProps, "open" | "onOpenChange">) {
const utils = trpc.useContext();
const { t } = useLocale();
const router = useRouter();
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => {
await utils.viewer.eventTypes.invalidate();
showToast(t("event_type_deleted_successfully"), "success");
router.push("/event-types");
onOpenChange?.(false);
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
onOpenChange?.(false);
} else if (err instanceof TRPCClientError) {
showToast(err.message, "error");
}
},
});
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<ConfirmationDialogContent
isPending={deleteMutation.isPending}
variety="danger"
title={t(`delete${isManagedEvent}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
onConfirm={(e) => {
e.preventDefault();
deleteMutation.mutate({ id: eventTypeId });
}}>
<p className="mt-5">
<Trans
i18nKey={`delete${isManagedEvent}_event_type_description`}
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
<ul>
<li>Members assigned to this event type will also have their event types deleted.</li>
<li>Anyone who they&apos;ve shared their link with will no longer be able to book using it.</li>
</ul>
</Trans>
</p>
</ConfirmationDialogContent>
</Dialog>
);
}
function EventTypeSingleLayout({
children,
eventType,
@@ -136,47 +202,35 @@ function EventTypeSingleLayout({
availability,
isUserOrganizationAdmin,
bookerUrl,
activeWebhooksNumber,
}: Props) {
const utils = trpc.useContext();
const { t } = useLocale();
const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const hasPermsToDelete =
currentUserMembership?.role !== "MEMBER" ||
!currentUserMembership ||
eventType.schedulingType === SchedulingType.MANAGED ||
formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
isUserOrganizationAdmin;
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => {
await utils.viewer.eventTypes.invalidate();
showToast(t("event_type_deleted_successfully"), "success");
router.push("/event-types");
setDeleteDialogOpen(false);
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
setDeleteDialogOpen(false);
} else if (err instanceof TRPCClientError) {
showToast(err.message, "error");
}
},
});
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
formMethods.getValues(),
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const length = formMethods.watch("length");
const multipleDuration = formMethods.watch("metadata")?.multipleDuration;
const watchSchedulingType = formMethods.watch("schedulingType");
const watchChildrenCount = formMethods.watch("children").length;
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
const navigation = getNavigation({
t,
eventType,
length,
multipleDuration,
id: formMethods.getValues("id"),
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
@@ -185,31 +239,29 @@ function EventTypeSingleLayout({
navigation.splice(1, 0, {
name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`,
href: `/event-types/${formMethods.getValues("id")}?tabName=availability`,
icon: Calendar,
info:
isManagedEventType || isChildrenManagedEventType
? eventType.schedule === null
? formMethods.getValues("schedule") === null
? "members_default_schedule"
: isChildrenManagedEventType
? `${
eventType.scheduleName
? `${eventType.scheduleName} - ${t("managed")}`
formMethods.getValues("scheduleName")
? `${formMethods.getValues("scheduleName")} - ${t("managed")}`
: `default_schedule_name`
}`
: eventType.scheduleName ?? `default_schedule_name`
: eventType.scheduleName ?? `default_schedule_name`,
: formMethods.getValues("scheduleName") ?? `default_schedule_name`
: formMethods.getValues("scheduleName") ?? `default_schedule_name`,
});
// If there is a team put this navigation item within the tabs
if (team) {
navigation.splice(2, 0, {
name: "assignment",
href: `/event-types/${eventType.id}?tabName=team`,
href: `/event-types/${formMethods.getValues("id")}?tabName=team`,
icon: Users,
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
isManagedEventType
? ` - ${t("number_member", { count: formMethods.watch("children").length || 0 })}`
: ""
info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
isManagedEventType ? ` - ${t("number_member", { count: watchChildrenCount || 0 })}` : ""
}`,
});
}
@@ -225,15 +277,14 @@ function EventTypeSingleLayout({
}
navigation.push({
name: "webhooks",
href: `/event-types/${eventType.id}?tabName=webhooks`,
href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
icon: TbWebhook,
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
info: `${activeWebhooksNumber} ${t("active")}`,
});
}
return navigation;
}, [
t,
eventType,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
@@ -241,16 +292,23 @@ function EventTypeSingleLayout({
isManagedEventType,
isChildrenManagedEventType,
team,
formMethods,
length,
multipleDuration,
formMethods.getValues("id"),
watchSchedulingType,
watchChildrenCount,
activeWebhooksNumber,
]);
const permalink = `${bookerUrl}/${
team ? `${!team.parentId ? "team/" : ""}${team.slug}` : eventType.users[0].username
team ? `${!team.parentId ? "team/" : ""}${team.slug}` : formMethods.getValues("users")[0].username
}/${eventType.slug}`;
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
const embedLink = `${
team ? `team/${team.slug}` : formMethods.getValues("users")[0].username
}/${formMethods.getValues("slug")}`;
const isManagedEvent = formMethods.getValues("schedulingType") === SchedulingType.MANAGED ? "_managed" : "";
// const title = formMethods.watch("title");
return (
<Shell
backPath="/event-types"
@@ -258,7 +316,7 @@ function EventTypeSingleLayout({
heading={eventType.title}
CTA={
<div className="flex items-center justify-end">
{!eventType.metadata?.managedEventConfig && (
{!formMethods.getValues("metadata")?.managedEventConfig && (
<>
<div
className={classNames(
@@ -285,7 +343,7 @@ function EventTypeSingleLayout({
id="hiddenSwitch"
checked={!formMethods.watch("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", !e);
formMethods.setValue("hidden", !e, { shouldDirty: true });
}}
/>
</div>
@@ -329,11 +387,11 @@ function EventTypeSingleLayout({
StartIcon={Code}
color="secondary"
variant="icon"
namespace={eventType.slug}
namespace=""
tooltip={t("embed")}
tooltipSide="bottom"
tooltipOffset={4}
eventId={eventType.id}
eventId={formMethods.getValues("id")}
/>
</>
)}
@@ -401,7 +459,7 @@ function EventTypeSingleLayout({
id="hiddenSwitch"
checked={!formMethods.watch("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", !e);
formMethods.setValue("hidden", !e, { shouldDirty: true });
}}
/>
</div>
@@ -412,6 +470,7 @@ function EventTypeSingleLayout({
className="ml-4 lg:ml-0"
type="submit"
loading={isUpdateMutationLoading}
disabled={!formMethods.formState.isDirty}
data-testid="update-eventtype"
form="event-type-form">
{t("save")}
@@ -444,31 +503,13 @@ function EventTypeSingleLayout({
</div>
</div>
</Suspense>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
variety="danger"
title={t(`delete${isManagedEvent}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
onConfirm={(e) => {
e.preventDefault();
deleteMutation.mutate({ id: eventType.id });
}}>
<p className="mt-5">
<Trans
i18nKey={`delete${isManagedEvent}_event_type_description`}
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
<ul>
<li>Members assigned to this event type will also have their event types deleted.</li>
<li>
Anyone who they&apos;ve shared their link with will no longer be able to book using it.
</li>
</ul>
</Trans>
</p>
</ConfirmationDialogContent>
</Dialog>
<DeleteDialog
eventTypeId={eventType.id}
isManagedEvent={isManagedEvent}
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
/>
<EventTypeEmbedDialog />
</Shell>
);
@@ -35,8 +35,9 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
async onSuccess() {
setEditModalOpen(false);
await utils.viewer.webhook.list.invalidate();
showToast(t("webhook_updated_successfully"), "success");
await utils.viewer.webhook.list.invalidate();
await utils.viewer.eventTypes.get.invalidate();
},
onError(error) {
showToast(`${error.message}`, "error");
@@ -45,9 +46,10 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
async onSuccess() {
setCreateModalOpen(false);
showToast(t("webhook_created_successfully"), "success");
await utils.viewer.webhook.list.invalidate();
setCreateModalOpen(false);
await utils.viewer.eventTypes.get.invalidate();
},
onError(error) {
showToast(`${error.message}`, "error");
@@ -81,10 +81,10 @@ export default function InstantEventController({
data-testid="instant-event-check"
onCheckedChange={(e) => {
if (!e) {
formMethods.setValue("isInstantEvent", false);
formMethods.setValue("isInstantEvent", false, { shouldDirty: true });
setInstantEventState(false);
} else {
formMethods.setValue("isInstantEvent", true);
formMethods.setValue("isInstantEvent", true, { shouldDirty: true });
setInstantEventState(true);
}
}}>
@@ -108,7 +108,7 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
eventTypeId: eventType.id,
eventTriggers: [WebhookTriggerEvents.INSTANT_MEETING],
});
const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({
const { data: installedApps, isPending } = trpc.viewer.integrations.useQuery({
variant: "other",
onlyInstalled: true,
});
@@ -188,7 +188,7 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
return (
<div>
{webhooks && !isLoading && (
{webhooks && !isPending && (
<>
<div>
{webhooks.length ? (
@@ -8,18 +8,18 @@ interface ManagedEventDialogProps {
slugExistsChildrenDialogOpen: ChildrenEventType[];
slug: string;
onOpenChange: () => void;
isLoading: boolean;
isPending: boolean;
onConfirm: (e: { preventDefault: () => void }) => void;
}
export default function ManagedEventDialog(props: ManagedEventDialogProps) {
const { t } = useLocale();
const { slugExistsChildrenDialogOpen, slug, onOpenChange, isLoading, onConfirm } = props;
const { slugExistsChildrenDialogOpen, slug, onOpenChange, isPending, onConfirm } = props;
return (
<Dialog open={slugExistsChildrenDialogOpen.length > 0} onOpenChange={onOpenChange}>
<ConfirmationDialogContent
isLoading={isLoading}
isPending={isPending}
variety="warning"
title={t("managed_event_dialog_title", {
slug,
@@ -19,11 +19,10 @@ export default function RecurringEventController({
paymentEnabled,
}: RecurringEventControllerProps) {
const { t } = useLocale();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
eventType.recurringEvent
);
const formMethods = useFormContext<FormValues>();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
formMethods.getValues("recurringEvent")
);
/* Just yearly-0, monthly-1 and weekly-2 */
const recurringEventFreqOptions = Object.entries(Frequency)
.filter(([key, value]) => isNaN(Number(key)) && Number(value) < 3)
@@ -67,7 +66,7 @@ export default function RecurringEventController({
data-testid="recurring-event-check"
onCheckedChange={(e) => {
if (!e) {
formMethods.setValue("recurringEvent", null);
formMethods.setValue("recurringEvent", null, { shouldDirty: true });
setRecurringEventState(null);
} else {
const newVal = eventType.recurringEvent || {
@@ -75,7 +74,7 @@ export default function RecurringEventController({
count: 12,
freq: Frequency.WEEKLY,
};
formMethods.setValue("recurringEvent", newVal);
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
setRecurringEventState(newVal);
}
}}>
@@ -96,7 +95,7 @@ export default function RecurringEventController({
...recurringEventState,
interval: parseInt(event?.target.value),
};
formMethods.setValue("recurringEvent", newVal);
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
setRecurringEventState(newVal);
}}
/>
@@ -111,7 +110,7 @@ export default function RecurringEventController({
...recurringEventState,
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
};
formMethods.setValue("recurringEvent", newVal);
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
setRecurringEventState(newVal);
}}
/>
@@ -130,7 +129,7 @@ export default function RecurringEventController({
...recurringEventState,
count: parseInt(event?.target.value),
};
formMethods.setValue("recurringEvent", newVal);
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
setRecurringEventState(newVal);
}}
/>
@@ -37,7 +37,7 @@ export default function RequiresConfirmationController({
useEffect(() => {
if (!requiresConfirmation) {
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined, { shouldDirty: true });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [requiresConfirmation]);
@@ -75,13 +75,14 @@ export default function RequiresConfirmationController({
)}
childrenClassName="lg:ml-0"
title={t("requires_confirmation")}
data-testid="requires-confirmation"
disabled={seatsEnabled || requiresConfirmationLockedProps.disabled}
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
description={t("requires_confirmation_description")}
checked={requiresConfirmation}
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
onCheckedChange={(val) => {
formMethods.setValue("requiresConfirmation", val);
formMethods.setValue("requiresConfirmation", val, { shouldDirty: true });
onRequiresConfirmation(val);
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
@@ -95,16 +96,19 @@ export default function RequiresConfirmationController({
}
onValueChange={(val) => {
if (val === "always") {
formMethods.setValue("requiresConfirmation", true);
formMethods.setValue("requiresConfirmation", true, { shouldDirty: true });
onRequiresConfirmation(true);
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined, {
shouldDirty: true,
});
setRequiresConfirmationSetup(undefined);
} else if (val === "notice") {
formMethods.setValue("requiresConfirmation", true);
formMethods.setValue("requiresConfirmation", true, { shouldDirty: true });
onRequiresConfirmation(true);
formMethods.setValue(
"metadata.requiresConfirmationThreshold",
requiresConfirmationSetup || defaultRequiresConfirmationSetup
requiresConfirmationSetup || defaultRequiresConfirmationSetup,
{ shouldDirty: true }
);
}
}}>
@@ -145,7 +149,8 @@ export default function RequiresConfirmationController({
});
formMethods.setValue(
"metadata.requiresConfirmationThreshold.time",
val
val,
{ shouldDirty: true }
);
}}
className="border-default !m-0 block w-16 rounded-r-none border-r-0 text-sm [appearance:textfield] focus:z-10 focus:border-r"
@@ -170,7 +175,8 @@ export default function RequiresConfirmationController({
});
formMethods.setValue(
"metadata.requiresConfirmationThreshold.unit",
opt?.value as UnitTypeLongPlural
opt?.value as UnitTypeLongPlural,
{ shouldDirty: true }
);
}}
defaultValue={defaultValue}
@@ -28,7 +28,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
color="secondary"
disabled={installed}
type="button"
loading={buttonProps?.isLoading}
loading={buttonProps?.isPending}
onClick={(event) => {
// Save cookie key to return url step
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
@@ -31,7 +31,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
return (
<>
{/* Already connected calendars */}
{!queryConnectedCalendars.isLoading &&
{!queryConnectedCalendars.isPending &&
firstCalendar &&
firstCalendar.integration &&
firstCalendar.integration.title &&
@@ -76,7 +76,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
</List>
)}
{queryIntegrations.isLoading && <StepConnectionLoader />}
{queryIntegrations.isPending && <StepConnectionLoader />}
<button
type="button"
@@ -13,7 +13,7 @@ interface ConnectedAppStepProps {
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
const { nextStep } = props;
const { data: queryConnectedVideoApps, isLoading } = trpc.viewer.integrations.useQuery({
const { data: queryConnectedVideoApps, isPending } = trpc.viewer.integrations.useQuery({
variant: "conferencing",
onlyInstalled: false,
sortByMostPopular: true,
@@ -26,7 +26,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
return (
<>
{!isLoading && (
{!isPending && (
<List className="bg-default border-subtle divide-subtle scroll-bar mx-1 max-h-[45vh] divide-y !overflow-y-scroll rounded-md border p-0 sm:mx-0">
{queryConnectedVideoApps?.items &&
queryConnectedVideoApps?.items.map((item) => {
@@ -48,7 +48,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
</List>
)}
{isLoading && <StepConnectionLoader />}
{isPending && <StepConnectionLoader />}
<button
type="button"
data-testid="save-video-button"
@@ -8,7 +8,6 @@ import { md } from "@calcom/lib/markdownIt";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import turndown from "@calcom/lib/turndownService";
import { trpc } from "@calcom/trpc/react";
import type { Ensure } from "@calcom/types/utils";
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
import { UserAvatar } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
@@ -97,18 +96,10 @@ const UserProfile = () => {
},
];
const organization =
user.organization && user.organization.id
? {
...(user.organization as Ensure<typeof user.organization, "id">),
slug: user.organization.slug || null,
requestedSlug: user.organization.metadata?.requestedSlug || null,
}
: null;
return (
<form onSubmit={onSubmit}>
<div className="flex flex-row items-center justify-start rtl:justify-end">
{user && <UserAvatar size="lg" user={user} previewSrc={imageSrc} organization={organization} />}
{user && <UserAvatar size="lg" user={user} previewSrc={imageSrc} />}
<input
ref={avatarRef}
type="hidden"
@@ -113,7 +113,7 @@ const UserSettings = (props: IUserSettingsProps) => {
<Button
type="submit"
className="mt-8 flex w-full flex-row justify-center"
disabled={mutation.isLoading}>
disabled={mutation.isPending}>
{t("next_step_text")}
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</Button>

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