Compare commits

..
196 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
634 changed files with 22390 additions and 9638 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.
+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"),
})
);
@@ -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 =
+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">;
@@ -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">;
@@ -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";
@@ -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([
@@ -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";
@@ -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>
@@ -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,8 +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 { IS_VISUAL_REGRESSION_TESTING } 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";
@@ -32,6 +31,7 @@ import {
Label,
SettingsToggle,
showToast,
Switch,
TextField,
Tooltip,
} from "@calcom/ui";
@@ -41,22 +41,17 @@ 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(!!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,7 +60,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const selectedThemeIsDark =
user?.theme === "dark" ||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
formMethods.getValues().bookingFields.forEach(({ name }) => {
bookingFields[name] = `${name} input`;
});
@@ -82,7 +76,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const [requiresConfirmation, setRequiresConfirmation] = useState(
formMethods.getValues("requiresConfirmation")
);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
const placeholderHashedLink = `${WEBSITE_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const noShowFeeEnabled =
formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
@@ -107,7 +101,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
};
}
return field;
})
}),
{ shouldDirty: true }
);
};
@@ -125,6 +120,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
const closeEventNameTip = () => setShowEventNameTip(false);
const displayDestinationCalendarSelector =
!!connectedCalendarsQuery.data?.connectedCalendars.length && !team;
return (
<div className="flex flex-col space-y-4">
@@ -134,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>
@@ -180,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} />
@@ -215,6 +233,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
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}
@@ -231,6 +250,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
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}
@@ -252,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}
@@ -266,6 +287,7 @@ 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"
{...formMethods.register("successRedirectUrl")}
@@ -274,7 +296,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
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>
@@ -295,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">
@@ -305,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">
@@ -369,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", eventType.seatsPerTimeSlot ?? 2);
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2, {
shouldDirty: true,
});
} else {
formMethods.setValue("seatsPerTimeSlot", null);
toggleGuests(true);
@@ -398,12 +424,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
onChange={(e) => {
onChange(Math.abs(Number(e.target.value)));
}}
data-testid="seats-per-time-slot"
/>
<div className="mt-4">
<Controller
name="seatsShowAttendees"
render={({ field: { value, onChange } }) => (
<CheckboxField
data-testid="show-attendees"
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => onChange(e)}
@@ -446,6 +474,7 @@ 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"
/>
)}
/>
@@ -489,7 +518,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
{showEventNameTip && (
<CustomEventTypeModal
close={closeEventNameTip}
setValue={(val: string) => formMethods.setValue("eventName", val)}
setValue={(val: string) => formMethods.setValue("eventName", val, { shouldDirty: true })}
defaultValue={formMethods.getValues("eventName")}
placeHolder={eventNamePlaceholder}
event={eventNameObject}
@@ -31,10 +31,14 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
const allAppsData = formMethods.watch("metadata")?.apps || {};
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
formMethods.setValue("metadata", {
...formMethods.getValues("metadata"),
apps: _allAppsData,
});
formMethods.setValue(
"metadata",
{
...formMethods.getValues("metadata"),
apps: _allAppsData,
},
{ shouldDirty: true }
);
};
const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => {
@@ -230,7 +230,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
);
setValue("availability", value);
setValue("availability", value, { shouldDirty: true });
},
[data]
);
@@ -238,7 +238,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
useEffect(() => {
if (!availabilityValue?.value) return;
setValue("schedule", availabilityValue.value);
setValue("schedule", availabilityValue.value, { shouldDirty: true });
}, [availabilityValue, setValue]);
return (
@@ -260,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}
@@ -298,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")}
@@ -61,7 +61,8 @@ const MinimumBookingNoticeInput = React.forwardRef<
minimumBookingNoticeDisplayValues.type,
"minutes",
minimumBookingNoticeDisplayValues.value
)
),
{ shouldDirty: true }
);
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
@@ -146,9 +147,9 @@ export const EventLimitsTab = () => {
<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"
name="beforeEventBuffer"
render={({ field: { onChange, value } }) => {
const beforeBufferOptions = [
{
@@ -176,9 +177,9 @@ export const EventLimitsTab = () => {
/>
</div>
<div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")}</Label>
<Label htmlFor="afterEventBuffer">{t("after_event")}</Label>
<Controller
name="afterBufferTime"
name="afterEventBuffer"
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
@@ -230,7 +231,9 @@ export const EventLimitsTab = () => {
<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(
@@ -258,11 +261,15 @@ export const EventLimitsTab = () => {
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(
@@ -279,7 +286,7 @@ export const EventLimitsTab = () => {
/>
<Controller
name="onlyShowFirstAvailableSlot"
render={({ field: { value } }) => {
render={({ field: { onChange, value } }) => {
const isChecked = value;
return (
<SettingsToggle
@@ -289,7 +296,7 @@ export const EventLimitsTab = () => {
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",
@@ -301,7 +308,7 @@ export const EventLimitsTab = () => {
/>
<Controller
name="durationLimits"
render={({ field: { value } }) => {
render={({ field: { onChange, value } }) => {
const isChecked = Object.keys(value ?? {}).length > 0;
return (
<SettingsToggle
@@ -317,11 +324,11 @@ export const EventLimitsTab = () => {
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">
@@ -338,7 +345,7 @@ export const EventLimitsTab = () => {
/>
<Controller
name="periodType"
render={({ field: { value } }) => {
render={({ field: { onChange, value } }) => {
const isChecked = value && value !== "UNLIMITED";
return (
@@ -353,11 +360,16 @@ export const EventLimitsTab = () => {
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
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 (
@@ -388,17 +400,21 @@ export const EventLimitsTab = () => {
options={optionsPeriod}
isSearchable={false}
onChange={(opt) =>
formMethods.setValue("periodCountCalendarDays", opt?.value === 1 ? "1" : "0")
formMethods.setValue(
"periodCountCalendarDays",
opt?.value === 1 ? true : false,
{ shouldDirty: true }
)
}
name="periodCoundCalendarDays"
value={optionsPeriod.find((opt) => {
opt.value ===
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0);
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0);
})}
defaultValue={optionsPeriod.find(
(opt) =>
opt.value ===
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0)
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0)
)}
/>
</div>
@@ -407,12 +423,12 @@ export const EventLimitsTab = () => {
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
<Controller
name="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,
});
@@ -446,7 +462,7 @@ export const EventLimitsTab = () => {
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">
@@ -577,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 (
@@ -611,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;
+14 -13
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";
@@ -83,7 +83,7 @@ const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
return mounted ? (
<Editor
getText={() => md.render(formMethods.getValues("description") || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
setText={(value: string) => formMethods.setValue("description", turndown(value), { shouldDirty: true })}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
editable={isEditable}
@@ -401,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/${formMethods.getValues("id")}?tabName=advanced`}
href={`${WEBAPP_URL}/event-types/${formMethods.getValues("id")}?tabName=advanced`}
className="underline">
here.
</Link>{" "}
@@ -412,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>
@@ -444,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>
@@ -511,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>
@@ -541,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>
@@ -570,12 +571,12 @@ export const EventSetupTab = (
setMultipleDuration(undefined);
setSelectedMultipleDuration([]);
setDefaultDuration(null);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
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 });
}
}}
/>
+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,
@@ -133,6 +134,60 @@ function getNavigation({
];
}
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,
@@ -149,9 +204,7 @@ function EventTypeSingleLayout({
bookerUrl,
activeWebhooksNumber,
}: Props) {
const utils = trpc.useContext();
const { t } = useLocale();
const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const hasPermsToDelete =
@@ -160,24 +213,6 @@ function EventTypeSingleLayout({
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(
formMethods.getValues(),
t("locked_fields_admin_description"),
@@ -308,7 +343,7 @@ function EventTypeSingleLayout({
id="hiddenSwitch"
checked={!formMethods.watch("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", !e);
formMethods.setValue("hidden", !e, { shouldDirty: true });
}}
/>
</div>
@@ -352,7 +387,7 @@ function EventTypeSingleLayout({
StartIcon={Code}
color="secondary"
variant="icon"
namespace={eventType.slug}
namespace=""
tooltip={t("embed")}
tooltipSide="bottom"
tooltipOffset={4}
@@ -424,7 +459,7 @@ function EventTypeSingleLayout({
id="hiddenSwitch"
checked={!formMethods.watch("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", !e);
formMethods.setValue("hidden", !e, { shouldDirty: true });
}}
/>
</div>
@@ -435,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")}
@@ -467,31 +503,13 @@ function EventTypeSingleLayout({
</div>
</div>
</Suspense>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<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: formMethods.getValues("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>
);
@@ -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);
}
}}>
@@ -66,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 || {
@@ -74,7 +74,7 @@ export default function RecurringEventController({
count: 12,
freq: Frequency.WEEKLY,
};
formMethods.setValue("recurringEvent", newVal);
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
setRecurringEventState(newVal);
}
}}>
@@ -95,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);
}}
/>
@@ -110,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);
}}
/>
@@ -129,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}
@@ -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"
+1 -1
View File
@@ -3,7 +3,7 @@ import classNames from "classnames";
import { signIn } from "next-auth/react";
import React from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import * as z from "zod";
import { z } from "zod";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { WEBSITE_URL } from "@calcom/lib/constants";
@@ -3,7 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { noop } from "lodash";
import { useCallback, useState } from "react";
import { Controller, FormProvider, useForm, useFormState } from "react-hook-form";
import * as z from "zod";
import { z } from "zod";
import { classNames } from "@calcom/lib";
import { CONSOLE_URL } from "@calcom/lib/constants";
+7 -2
View File
@@ -5,11 +5,16 @@ import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { md } from "@calcom/lib/markdownIt";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import type { UserProfile } from "@calcom/types/UserProfile";
import { UserAvatar } from "@calcom/ui";
type TeamType = Omit<NonNullable<TeamWithMembers>, "inviteToken">;
type MembersType = TeamType["members"];
type MemberType = Pick<MembersType[number], "id" | "name" | "bio" | "username" | "organizationId"> & {
type MemberType = Pick<
MembersType[number],
"id" | "name" | "bio" | "username" | "organizationId" | "avatarUrl"
> & {
profile: Omit<UserProfile, "upId">;
safeBio: string | null;
bookerUrl: string;
};
@@ -27,7 +32,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
key={member.id}
href={{ pathname: `${member.bookerUrl}/${member.username}`, query: queryParamsToForward }}>
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
<UserAvatar size="md" user={member} />
<UserAvatar noOrganizationIndicator size="md" user={member} />
<section className="mt-2 line-clamp-4 w-full space-y-1">
<p className="text-default font-medium">{member.name}</p>
<div className="text-subtle line-clamp-3 overflow-ellipsis text-sm font-normal">
@@ -4,7 +4,7 @@ import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
import { WEBSITE_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
@@ -45,7 +45,7 @@ export const UsernameAvailabilityField = ({
const usernamePrefix = orgBranding
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
: `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
: `${WEBSITE_URL?.replace(/^(https?:|)\/\//, "")}`;
return (
<Controller
@@ -34,6 +34,7 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
<Select<LocationOption>
name="location"
id="location-select"
data-testid="location-select"
components={{
Option: (props) => {
return (
+33 -15
View File
@@ -1,4 +1,5 @@
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
@@ -115,21 +116,23 @@ export const handleSeatsEventTypeOnBooking = async (
}>
>,
seatReferenceUid?: string,
userId?: number
isHost?: boolean
) => {
if (eventType?.seatsPerTimeSlot !== null) {
// @TODO: right now bookings with seats doesn't save every description that its entered by every user
delete bookingInfo.description;
} else {
return;
}
// @TODO: If handling teams, we need to do more check ups for this.
if (bookingInfo?.user?.id === userId) {
return;
}
if (!eventType.seatsShowAttendees) {
const seatAttendee = await prisma.bookingSeat.findFirst({
bookingInfo["responses"] = {};
type seatAttendee = {
attendee: {
email: string;
name: string;
};
id: number;
data: Prisma.JsonValue;
bookingId: number;
attendeeId: number;
referenceUid: string;
} | null;
let seatAttendee: seatAttendee = null;
if (seatReferenceUid) {
seatAttendee = await prisma.bookingSeat.findFirst({
where: {
referenceUid: seatReferenceUid,
},
@@ -142,16 +145,31 @@ export const handleSeatsEventTypeOnBooking = async (
},
},
});
}
if (seatAttendee) {
const seatAttendeeData = seatAttendee.data as unknown as {
description?: string;
responses: Prisma.JsonValue;
};
bookingInfo["description"] = seatAttendeeData.description ?? null;
bookingInfo["responses"] = bookingResponsesDbSchema.parse(seatAttendeeData.responses ?? {});
}
if (!eventType.seatsShowAttendees && !isHost) {
if (seatAttendee) {
const attendee = bookingInfo?.attendees?.find((a) => {
return a.email === seatAttendee.attendee?.email;
return a.email === seatAttendee?.attendee?.email;
});
bookingInfo["attendees"] = attendee ? [attendee] : [];
} else {
bookingInfo["attendees"] = [];
}
}
// // @TODO: If handling teams, we need to do more check ups for this.
// if (bookingInfo?.user?.id === userId) {
// return;
// }
return bookingInfo;
};
@@ -5,6 +5,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { UserRepository } from "@calcom/lib/server/repository/user";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
@@ -56,19 +57,9 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
return notFound;
}
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
},
select: {
away: true,
hideBranding: true,
},
const [user] = await UserRepository.findUsersByUsername({
usernameList: [username],
orgSlug: org,
});
if (!user) {
@@ -0,0 +1,21 @@
import type { GetServerSidePropsContext } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { ssrInit } from "@server/lib/ssr";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const session = await getServerSession({ req: context.req, res: context.res });
if (!session) {
return {
redirect: {
permanent: false,
destination: "/auth/login",
},
};
}
return { props: { trpcState: ssr.dehydrate() } };
};
+6 -1
View File
@@ -11,7 +11,12 @@ export default function useRouterQuery<T extends string>(name: T) {
const setQuery = useCallback(
(newValue: string | number | null | undefined) => {
const _searchParams = new URLSearchParams(searchParams ?? undefined);
_searchParams.set(name, newValue as string);
if (typeof newValue === "undefined") {
// when newValue is of type undefined, clear the search param.
_searchParams.delete(name);
} else {
_searchParams.set(name, newValue as string);
}
router.replace(`${pathname}?${_searchParams.toString()}`);
},
[name, pathname, router, searchParams]
@@ -5,9 +5,10 @@ import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/or
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { getServerSideProps as GSSUserTypePage } from "@lib/[user]/[type]/getServerSideProps";
import { getServerSideProps as GSSTeamTypePage } from "@lib/team/[slug]/[type]/getServerSideProps";
import { getServerSideProps as GSSUserTypePage } from "~/users/views/users-type-public-view.getServerSideProps";
const paramsSchema = z.object({
orgSlug: z.string().transform((s) => slugify(s)),
user: z.string(),
+5 -2
View File
@@ -1589,17 +1589,20 @@ async function createTeamOutsideOrg(
});
}
async function createUserOutsideOrg(data: Omit<Prisma.UserCreateArgs["data"], "organization">) {
async function createUserOutsideOrg(
data: Omit<Prisma.UserCreateArgs["data"], "organization" | "movedToProfile">
) {
return await prismock.user.create({
data: {
...data,
movedToProfileId: null,
organizationId: null,
},
});
}
async function createUserInsideTheOrg(
data: Omit<Prisma.UserUncheckedCreateInput, "organization" | "organizationId" | "id">,
data: Omit<Prisma.UserUncheckedCreateInput, "organization" | "organizationId" | "id" | "movedToProfileId">,
orgId: number
) {
const org = await prismock.team.findUnique({
+37 -3
View File
@@ -3,6 +3,7 @@ import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomai
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import prisma from "@calcom/prisma";
import type { Team, User } from "@calcom/prisma/client";
import { RedirectType } from "@calcom/prisma/client";
@@ -327,7 +328,7 @@ async function setOrgSlugIfNotSet(
slug: string | null;
},
orgMetadata: {
requestedSlug?: string | undefined;
requestedSlug?: string | null | undefined;
},
targetOrgId: number
) {
@@ -348,7 +349,7 @@ async function setOrgSlugIfNotSet(
function assertUserPartOfOrgAndRemigrationAllowed(
userToMoveToOrg: {
organizationId: User["organizationId"];
organizationId: number | null;
},
targetOrgId: number,
targetOrgUsername: string,
@@ -384,7 +385,7 @@ async function getTeamOrThrowError(targetOrgId: number) {
function assertUserPartOfOtherOrg(
userToMoveToOrg: {
organizationId: User["organizationId"];
organizationId: number | null;
} | null,
userName: string | undefined,
userId: number | undefined,
@@ -594,6 +595,35 @@ async function dbMoveUserToOrg({
},
},
});
await prisma.profile.upsert({
create: {
uid: ProfileRepository.generateProfileUid(),
userId: userToMoveToOrg.id,
organizationId: targetOrgId,
username: targetOrgUsername,
movedFromUser: {
connect: {
id: userToMoveToOrg.id,
},
},
},
update: {
organizationId: targetOrgId,
username: targetOrgUsername,
movedFromUser: {
connect: {
id: userToMoveToOrg.id,
},
},
},
where: {
userId_organizationId: {
userId: userToMoveToOrg.id,
organizationId: targetOrgId,
},
},
});
}
async function moveTeamsWithoutMembersToOrg({
@@ -841,6 +871,10 @@ async function dbRemoveUserFromOrg({
},
},
});
await ProfileRepository.deleteMany({
userIds: [userToRemoveFromOrg.id],
});
}
async function removeMembership({
@@ -2,7 +2,7 @@ import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
@@ -82,12 +82,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
entity: eventData.entity,
duration: getMultipleDurationValue(
eventData.metadata?.multipleDuration,
queryDuration,
eventData.length
),
eventData: {
entity: eventData.entity,
length: eventData.length,
metadata: eventData.metadata,
},
booking,
away: false,
user: teamSlug,
@@ -18,9 +18,21 @@ import { ssrInit } from "@server/lib/ssr";
const log = logger.getSubLogger({ prefix: ["team/[slug]"] });
const getTheLastArrayElement = (value: ReadonlyArray<string> | string | undefined): string | undefined => {
if (value === undefined || typeof value === "string") {
return value;
}
return value.at(-1);
};
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
const slug = getTheLastArrayElement(context.query.slug) ?? getTheLastArrayElement(context.query.orgSlug);
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(
context.req,
context.params?.orgSlug ?? context.query?.orgSlug
);
const isOrgContext = isValidOrgDomain && currentOrgDomain;
// Provided by Rewrite from next.config.js
@@ -109,7 +121,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
name: member.name,
id: member.id,
avatarUrl: member.avatarUrl,
bio: member.bio,
profile: member.profile,
subteams: member.subteams,
username: member.username,
accepted: member.accepted,
+23 -12
View File
@@ -11,6 +11,14 @@ import { csp } from "@lib/csp";
import { abTestMiddlewareFactory } from "./abTest/middlewareFactory";
const safeGet = async <T = any>(key: string): Promise<T | undefined> => {
try {
return get<T>(key);
} catch (error) {
// Don't crash if EDGE_CONFIG env var is missing
}
};
const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
const url = req.nextUrl;
const requestHeaders = new Headers(req.headers);
@@ -24,18 +32,12 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
//
// - For this reason our matchers are sufficient for an app-wide maintenance page.
//
try {
// Check whether the maintenance page should be shown
const isInMaintenanceMode = await get<boolean>("isInMaintenanceMode");
// If is in maintenance mode, point the url pathname to the maintenance page
if (isInMaintenanceMode) {
req.nextUrl.pathname = `/maintenance`;
return NextResponse.rewrite(req.nextUrl);
}
} catch (error) {
// show the default page if EDGE_CONFIG env var is missing,
// but log the error to the console
// console.error(error);
// Check whether the maintenance page should be shown
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
// If is in maintenance mode, point the url pathname to the maintenance page
if (isInMaintenanceMode) {
req.nextUrl.pathname = `/maintenance`;
return NextResponse.rewrite(req.nextUrl);
}
}
@@ -60,6 +62,14 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
requestHeaders.set("x-cal-timezone", req.headers.get("x-vercel-ip-timezone") ?? "");
}
if (url.pathname.startsWith("/api/auth/signup")) {
const isSignupDisabled = await safeGet<boolean>("isSignupDisabled");
// If is in maintenance mode, point the url pathname to the maintenance page
if (isSignupDisabled) {
return NextResponse.json({ error: "Signup is disabled" }, { status: 503 });
}
}
if (url.pathname.startsWith("/auth/login") || url.pathname.startsWith("/login")) {
// Use this header to actually enforce CSP, otherwise it is running in Report Only mode on all pages.
requestHeaders.set("x-csp-enforce", "true");
@@ -114,6 +124,7 @@ export const config = {
// https://github.com/vercel/next.js/discussions/42458
matcher: [
"/:path*/embed",
"/api/auth/signup",
"/api/trpc/:path*",
"/login",
"/auth/login",
@@ -0,0 +1 @@
export const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
@@ -3,7 +3,7 @@ import { z } from "zod";
import { getTranslations } from "@server/lib/getTranslations";
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
import { validStatuses } from "~/bookings/lib/validStatuses";
const querySchema = z.object({
status: z.enum(validStatuses),
@@ -0,0 +1,258 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { Fragment, useState } from "react";
import { z } from "zod";
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
import dayjs from "@calcom/dayjs";
import { FilterToggle } from "@calcom/features/bookings/components/FilterToggle";
import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
import { Alert, Button, EmptyScreen, HorizontalTabs } from "@calcom/ui";
import { Calendar } from "@calcom/ui/components/icon";
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
import useMeQuery from "@lib/hooks/useMeQuery";
import BookingListItem from "@components/booking/BookingListItem";
import SkeletonLoader from "@components/booking/SkeletonLoader";
import { validStatuses } from "~/bookings/lib/validStatuses";
type BookingListingStatus = z.infer<NonNullable<typeof filterQuerySchema>>["status"];
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
type RecurringInfo = {
recurringEventId: string | null;
count: number;
firstDate: Date | null;
bookings: { [key: string]: Date[] };
};
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{
name: "upcoming",
href: "/bookings/upcoming",
},
{
name: "unconfirmed",
href: "/bookings/unconfirmed",
},
{
name: "recurring",
href: "/bookings/recurring",
},
{
name: "past",
href: "/bookings/past",
},
{
name: "cancelled",
href: "/bookings/cancelled",
},
];
const descriptionByStatus: Record<NonNullable<BookingListingStatus>, string> = {
upcoming: "upcoming_bookings",
recurring: "recurring_bookings",
past: "past_bookings",
cancelled: "cancelled_bookings",
unconfirmed: "unconfirmed_bookings",
};
const querySchema = z.object({
status: z.enum(validStatuses),
});
export default function Bookings() {
const params = useParamsWithFallback();
const { data: filterQuery } = useFilterQuery();
const { status } = params ? querySchema.parse(params) : { status: "upcoming" as const };
const { t } = useLocale();
const user = useMeQuery().data;
const [isFiltersVisible, setIsFiltersVisible] = useState<boolean>(false);
const query = trpc.viewer.bookings.get.useInfiniteQuery(
{
limit: 10,
filters: {
...filterQuery,
status: filterQuery.status ?? status,
},
},
{
// first render has status `undefined`
enabled: true,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
// Animate page (tab) transitions to look smoothing
const buttonInView = useInViewObserver(() => {
if (!query.isFetching && query.hasNextPage && query.status === "success") {
query.fetchNextPage();
}
});
const isEmpty = !query.data?.pages[0]?.bookings.length;
const shownBookings: Record<string, BookingOutput[]> = {};
const filterBookings = (booking: BookingOutput) => {
if (status === "recurring" || status == "unconfirmed" || status === "cancelled") {
if (!booking.recurringEventId) {
return true;
}
if (
shownBookings[booking.recurringEventId] !== undefined &&
shownBookings[booking.recurringEventId].length > 0
) {
shownBookings[booking.recurringEventId].push(booking);
return false;
}
shownBookings[booking.recurringEventId] = [booking];
} else if (status === "upcoming") {
return (
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") !==
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
);
}
return true;
};
let recurringInfoToday: RecurringInfo | undefined;
const bookingsToday =
query.data?.pages.map((page) =>
page.bookings.filter((booking: BookingOutput) => {
recurringInfoToday = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") ===
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
);
})
)[0] || [];
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
return (
<Shell
withoutMain={false}
hideHeadingOnMobile
heading={t("bookings")}
subtitle={t("bookings_description")}
title="Bookings"
description="Create events to share for people to book on your calendar.">
<div className="flex flex-col">
<div className="flex flex-row flex-wrap justify-between">
<HorizontalTabs tabs={tabs} />
<FilterToggle setIsFiltersVisible={setIsFiltersVisible} />
</div>
<FiltersContainer isFiltersVisible={isFiltersVisible} />
<main className="w-full">
<div className="flex w-full flex-col" ref={animationParentRef}>
{query.status === "error" && (
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
)}
{(query.status === "pending" || query.isPaused) && <SkeletonLoader />}
{query.status === "success" && !isEmpty && (
<>
{!!bookingsToday.length && status === "upcoming" && (
<div className="mb-6 pt-2 xl:pt-0">
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
<p className="text-subtle mb-2 text-xs font-medium uppercase leading-4">{t("today")}</p>
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="today-bookings">
<Fragment>
{bookingsToday.map((booking: BookingOutput) => (
<BookingListItem
key={booking.id}
loggedInUser={{
userId: user?.id,
userTimeZone: user?.timeZone,
userTimeFormat: user?.timeFormat,
userEmail: user?.email,
}}
listingStatus={status}
recurringInfo={recurringInfoToday}
{...booking}
/>
))}
</Fragment>
</tbody>
</table>
</div>
</div>
)}
<div className="pt-2 xl:pt-0">
<div className="border-subtle overflow-hidden rounded-md border">
<table data-testid={`${status}-bookings`} className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
const recurringInfo = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
<BookingListItem
key={booking.id}
loggedInUser={{
userId: user?.id,
userTimeZone: user?.timeZone,
userTimeFormat: user?.timeFormat,
userEmail: user?.email,
}}
listingStatus={status}
recurringInfo={recurringInfo}
{...booking}
/>
);
})}
</Fragment>
))}
</tbody>
</table>
</div>
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
</>
)}
{query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon={Calendar}
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(),
description: t(descriptionByStatus[status]),
})}
/>
</div>
)}
</div>
</main>
</div>
</Shell>
);
}

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