Compare commits

..
298 Commits
Author SHA1 Message Date
Omar López 37f01db0da v3.3.2
release.yml / release (push) Canceled after 0s
2023-09-19 14:03:20 -07:00
Omar López 560ec244e8 feat: Setups prisma accelerate (#11324) 2023-09-19 21:02:57 +00:00
Crowdin Bot 21a0b67b5a New Crowdin translations by Github Action 2023-09-19 20:49:32 +00:00
824145b0e2 feat: Sync app credentials between Cal.com & self-hosted platforms (#11059)
* Add credential sync .env variables

* Add webhook to send app credentials

* Upsert credentials when webhook called

* Refresh oauth token from a specific endpoint

* Pass appSlug

* Add credential encryption

* Move oauth helps into a folder

* Create parse token response wrapper

* Add OAuth helpers to apps

* Clean up

* Refactor `appDirName` to `appSlug`

* Address feedback

* Change to safe parse

* Remove console.log

---------

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2023-09-19 20:46:50 +00:00
Leo Giovanetti bc89fe00ea fix: unpublished org/team avatar and i18n (#11429)
* fix: unpublished org/team avatar and i18n

* Fixing e2e
2023-09-20 01:45:39 +05:30
Crowdin Bot 64850b54f0 New Crowdin translations by Github Action 2023-09-19 20:04:11 +00:00
405a4cc850 fix: Time Buttons Should be Scroll not the Whole Right Side (#11412)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-09-19 22:01:26 +02:00
Crowdin Bot eccab37607 New Crowdin translations by Github Action 2023-09-19 14:24:18 +00:00
786c1c2ba3 feat: monthly email digest (#10621)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-19 16:19:36 +02:00
Udit TakkarandLeo Giovanetti 50970dc249 fix: org invite (#11356)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-09-19 15:46:33 +05:30
Crowdin Bot 45cded6b48 New Crowdin translations by Github Action 2023-09-19 10:02:00 +00:00
Crowdin Bot b139ec8994 New Crowdin translations by Github Action 2023-09-19 09:58:12 +00:00
Peer Richelsen d058d322bc chore: slow caldav hint in description (#11438) 2023-09-19 11:54:51 +02:00
Leo Giovanetti 72907e5e1b feat: resend invitation (#11428) 2023-09-19 00:24:43 +00:00
Kevin Crimi d9c3c184c4 fix: Return all responses in booking api (#11284) 2023-09-18 16:39:25 -07:00
Crowdin Bot b1fff71c91 New Crowdin translations by Github Action 2023-09-18 23:38:32 +00:00
alannnc 56b1243e59 fix: re-render on insights with orgs (#11395) 2023-09-18 16:36:07 -07:00
Crowdin Bot a3413216f6 New Crowdin translations by Github Action 2023-09-18 19:19:24 +00:00
Shivam KalraandSyed Ali Shahbaz fd507f1d12 test: e2e oidc (#11129)
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2023-09-18 21:16:32 +02:00
Peer RichelsenandLeo Giovanetti c820a3937a chore: send comment about merge conflict (#11345)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-09-18 21:16:22 +02:00
aar2dee2andPeer Richelsen 5c24ed2ef7 chore: app contribution guidelines (#11416)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-18 21:16:02 +02:00
GitStart-Cal.comandgitstart-calcom 8d9d7bbb25 fix: Required "Add guests" button isn't required on attendee's end (#11381)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-09-18 21:15:40 +02:00
Crowdin Bot 837bf5d604 New Crowdin translations by Github Action 2023-09-18 16:01:33 +00:00
Hariom Balhara c9c634cceb Support firstName and lastName query params (#11420) 2023-09-18 21:28:07 +05:30
Hariom BalharaandJoe Au-Yeung 8ba09ce04d Fix messaging apps listing in installed (#11167)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-09-18 21:26:49 +05:30
Crowdin Bot 65d4a4f40e New Crowdin translations by Github Action 2023-09-18 07:55:08 +00:00
Crowdin Bot 581d8ff4ae New Crowdin translations by Github Action 2023-09-18 07:51:56 +00:00
Pradip Chaudhary c562f45904 fix: "scrollbar-gutter: stable" on html to prevent layout shift (#11413) 2023-09-18 09:49:01 +02:00
zomars b413f0a609 fix: add missing await 2023-09-15 17:22:50 -07:00
Crowdin Bot ffcf9b770d New Crowdin translations by Github Action 2023-09-15 19:27:35 +00:00
Hariom Balhara ab77531167 fix: Email Embed slots not showing up (#11379) 2023-09-15 12:24:20 -07:00
Crowdin Bot f59f4ca26c New Crowdin translations by Github Action 2023-09-15 16:22:47 +00:00
Crowdin Bot ca13eec4c9 New Crowdin translations by Github Action 2023-09-15 16:18:56 +00:00
Crowdin Bot bf1ff9bd71 New Crowdin translations by Github Action 2023-09-15 16:14:12 +00:00
Crowdin Bot caefa93e14 New Crowdin translations by Github Action 2023-09-15 16:10:36 +00:00
Leo Giovanetti 7154349e9e fix: org username check for dynamic and signup (#11390) 2023-09-15 13:06:56 -03:00
Crowdin Bot 7beb9568ba New Crowdin translations by Github Action 2023-09-15 15:56:09 +00:00
Crowdin Bot 2251d5a4be New Crowdin translations by Github Action 2023-09-15 15:53:15 +00:00
Crowdin Bot f1bdff9679 New Crowdin translations by Github Action 2023-09-15 15:50:25 +00:00
Crowdin Bot c433e0c74c New Crowdin translations by Github Action 2023-09-15 15:46:48 +00:00
Crowdin Bot fb940a1023 New Crowdin translations by Github Action 2023-09-15 15:43:05 +00:00
Crowdin Bot 73366e4152 New Crowdin translations by Github Action 2023-09-15 15:39:48 +00:00
Crowdin Bot 9d7da10744 New Crowdin translations by Github Action 2023-09-15 15:36:02 +00:00
Crowdin Bot c4f091debf New Crowdin translations by Github Action 2023-09-15 15:32:25 +00:00
Alex van Andel 29923fe304 v3.3.1
release.yml / release (push) Canceled after 0s
2023-09-15 16:28:09 +01:00
Richard PoelderlandAlex van Andel e8998414fc feat(i18n): changed Japanese translations on booking page (#11388)
* regen yarn.lock?

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

* Generated yarn.lock with submodules

* Added yarn.lock auth

* add datocms webhook secret

* update crowdin to latest version

* cherry-picked https://github.com/calcom/cal.com/pull/10416/files

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-09-15 16:25:49 +01:00
Richard PoelderlandAlex van Andel 2e58e73e95 chore: Update GH action from crowdin to latest version (#11387)
* regen yarn.lock?

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

* Generated yarn.lock with submodules

* Added yarn.lock auth

* add datocms webhook secret

* update crowdin to latest version

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-09-15 14:43:49 +00:00
Crowdin Bot b7168948ca New Crowdin translations by Github Action 2023-09-15 14:01:21 +00:00
sean-brydon c188a91d64 fix: Safari overflowing issues (#11385) 2023-09-15 15:57:49 +02:00
Crowdin Bot 4724b1312c New Crowdin translations by Github Action 2023-09-15 13:57:43 +00:00
Hariom Balhara d349f6536a fix: Embed - app.cal.com instead of cal.com (#11384)
* Fix yarn.lock

* Force app.cal.com because Safari and rewrites using referrer

* Add comment
2023-09-15 14:54:25 +01:00
Crowdin Bot f35f506ddf New Crowdin translations by Github Action 2023-09-15 13:43:44 +00:00
Crowdin Bot 46d0b67f81 New Crowdin translations by Github Action 2023-09-15 13:40:27 +00:00
Crowdin Bot 7000cf7581 New Crowdin translations by Github Action 2023-09-15 13:36:53 +00:00
Crowdin Bot 1c0faf4eb6 New Crowdin translations by Github Action 2023-09-15 13:33:26 +00:00
Alex van Andel 6d0f0d6bdd fix: Include returns all credential fields, select excludes (#11383) 2023-09-15 13:29:44 +00:00
Crowdin Bot cb4a3dd2a6 New Crowdin translations by Github Action 2023-09-15 13:25:56 +00:00
Crowdin Bot fc623fc511 New Crowdin translations by Github Action 2023-09-15 13:22:33 +00:00
Crowdin Bot 9db0834b50 New Crowdin translations by Github Action 2023-09-15 13:18:41 +00:00
Crowdin Bot 7a3cc7d263 New Crowdin translations by Github Action 2023-09-15 13:14:24 +00:00
Crowdin Bot f1381d8a82 New Crowdin translations by Github Action 2023-09-15 13:11:06 +00:00
Crowdin Bot 5c136df1bb New Crowdin translations by Github Action 2023-09-15 13:07:33 +00:00
Crowdin Bot e18f08249b New Crowdin translations by Github Action 2023-09-15 13:04:29 +00:00
Crowdin Bot 5b8b165a3b New Crowdin translations by Github Action 2023-09-15 13:01:21 +00:00
Somay ChauhanandShivam Kalra ab39c8f581 fix: a new email can signin with any password once it's invited to a team (#11359)
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-09-15 12:57:41 +00:00
Crowdin Bot abc077bfb6 New Crowdin translations by Github Action 2023-09-15 12:55:16 +00:00
Crowdin Bot bb167e7763 New Crowdin translations by Github Action 2023-09-15 12:52:03 +00:00
Crowdin Bot 3b92ff607a New Crowdin translations by Github Action 2023-09-15 12:47:56 +00:00
Crowdin Bot 129d00a69d New Crowdin translations by Github Action 2023-09-15 12:44:42 +00:00
Crowdin Bot c86e766aee New Crowdin translations by Github Action 2023-09-15 12:41:46 +00:00
Leo Giovanetti a2873c740b fix: team page data (#11382) 2023-09-15 13:38:55 +01:00
Crowdin Bot 6e3a1369f8 New Crowdin translations by Github Action 2023-09-15 11:55:51 +00:00
Chiranjeev Vishnoi e44e24ca42 fix: 2FA password visibility toggle show/hide state (#11361) 2023-09-15 17:22:58 +05:30
Somay Chauhan 768a3132fb fix: settings/organization/members table filtering issue (#11313) 2023-09-15 17:20:40 +05:30
Crowdin Bot 41812bb280 New Crowdin translations by Github Action 2023-09-15 10:19:46 +00:00
alannnc cb1d3487af chore: edit and update paypal setup instructions (#11325) 2023-09-15 15:46:28 +05:30
Omar López c95917cceb feat: Calendar Cache (#11185) 2023-09-14 15:19:39 -07:00
Omar López 9bc40a3eb6 v3.3.0
release.yml / release (push) Canceled after 0s
2023-09-14 12:50:43 -07:00
Keith Williamsandzomars bd21b2dfe1 fix: Import prisma (#11369)
Co-authored-by: zomars <zomars@me.com>
2023-09-14 19:47:25 +00:00
Crowdin Bot d60fb081c9 New Crowdin translations by Github Action 2023-09-14 16:56:47 +00:00
197b435b6f fix: Duplicate calendar events (#11327)
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Alan <alannnc@gmail.com>
2023-09-14 09:53:58 -07:00
dihedral add297b09a fix: use selected duration when checking for conflicts for new bookings (#10713) 2023-09-14 10:40:49 -04:00
Ujwal Kumar d65f0ee0e8 fix: Allow attendee first and last name from attendee name when name … (#11287) 2023-09-14 10:24:58 -04:00
Hariom Balhara 59c0a71db7 fix: Routing Forms - Forward Query Params and show the error message in case of validation error in headless router (#11338) 2023-09-14 10:05:22 -04:00
Hariom Balhara d50151fe08 Fix yarn.lock (#11364) 2023-09-14 13:41:56 +00:00
Kiran K d2b0dd8eab fix: SAML SSO - Stop relying on invitedTo column and use membership table instead (#11331) 2023-09-14 14:57:45 +02:00
Joe Au-Yeung bb0574dff8 chore: Outlook processBusyTimes iterate busy times once (#11350) 2023-09-14 14:56:31 +02:00
sean-brydon 49364eee9a Import prisma (#11358) 2023-09-14 09:18:19 -03:00
Peer Richelsen adb0530d69 feat: getalby.com app (#11347) 2023-09-14 08:35:13 -03:00
+18 a2d1dbebba feat: editmode availbility slider (#11197)
* Add loading data and banner

* [WIP] hasEditPerms middleware

* fix: type error in booker (#11011)

* New Crowdin translations by Github Action

* refactor: removed redundant test (#10785)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>

* feat: 2fa backup codes (#10600)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* Delete add-PRs-to-project-reviewing-PRs.yml (#11008)

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

* New Crowdin translations by Github Action

* fix: multiple duration when booking (#11032)

* fix: other reported issues (#11015)

* fix: weird margin top in avatar

* fix: pending users are shown on booking page

* fix: avatar and naming issues

* fix: toast alignment and removing unneeded titles

* missing changes from toast improvements

* feat: empty state for teams without event types

* Removing console.log

* feat: cal ai (#10992)

Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Co-authored-by: tedspare <ted.spare@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>

* New Crowdin translations by Github Action

* fix: meeting ended trigger for webhooks and zapier sometimes not working (#10946)

Co-authored-by: mohammed gehad <mohammed.gehad.1998@gmail.com>
Co-authored-by: Monto <138862352+monto7926@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* feat: team admin: see connected apps of team members (#11036)

* added feature:team admin can see connected apps of members

* fixed the type error

* Update packages/lib/server/queries/teams/index.ts

* Minor fixes

---------

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

* fix: lower case slugs in teams (#11026)

* fix: lower case slugs in teams

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

* fix: use slugify

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

---------

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

* refactor: improvements on german translation (#10898)

* fix: fix-tablet-menu-not-centered-sidebar (#11020)

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>

* chore: add Popover in storybook (#11021)

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

* fix: Set mobile availability (#11027)

* chore: add ColorPicker in storybook (CALCOM-10760) (#10866)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* feat: adds next cold start profiler (#11014)

* Handle disabling of ORGANIZATIONS_ENABLED flag (#11041)

* New Crowdin translations by Github Action

* styles:dark mode color fix (#11004)

* chore: sheet darkmode and improve responsive (#11047)

* fix: handle collective multiple host on destinationCalendar (#10967)

* fix: include app data and credentials from DB (#11048)

* include app data and credentials from DB

* Improve performance

* fix: Error when running storybook (#11037)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* fix: broken company profile link on team booking page (#10978)

* chore: add ErrorBoundary in storybook (CALCOM-10760) (#10872)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* feat: sorting for workflow and routing forms (#10780)

Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* resolve zod versions across child packages (#11052)

* fix: List storybook file is empty (fix-list) (#10965)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: admin org list without members (#11051)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* chore: add ToggleGroup in storybook (#10802)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

* fix: add metadata to stripe payment intent (#11053)

* fix: Logo storybook file with invalid icons (fix-logo) (#11018)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: Add controls for Select Field storybook file (#10936)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: Fix tooltip control on ButtonPlayground storybook file (fix-ButtonTooltip) (#10937)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: email embed – remove collapsible and permanently show times (#10996)

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

* chore: add Timezone Select in storybook (CALCOM-10760) (#10966)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* chore: add Switch in storybook (CALCOM-10760) (#10804)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* Sync packages

* fix: zod utils due to upgrade

* easy fix (#11054)

* fix sub quantity update stripe (#11057)

* v3.2.7

* Revert "feat: adds next cold start profiler (#11014)" (#11072)

This reverts commit 05631d093f.

* fix: Fixes username invite issue (#10998)

* Fixes username invite issue

* Ensure we only suggest email invites in org members

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>

* Fix sheet layout

* WIP permissions when userschedule doesnt match

* WIP get handler weird behaviour

* Update toast

* add disabled - handle membership overlap

* Handle permissions + perf improvments

* use input uid

* Remove Console.log

* Clean up

* Revert changes accidental

* Fix merge artifacts

* Remove dead code

* Remove code after return

* Update read permission check

* Revert avatar changes as fixed elsewhere

* Handle if user has not completed onboarding

* Disable button

* Update packages/lib/hasEditPermissionForUser.ts

* Correct Error throwing

* Update packages/features/timezone-buddy/components/AvailabilityEditSheet.tsx

* Fix type erro

* Add i18n

* Improve Spacing

* Update yarn.lock

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.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: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: DexterStorey <36115192+DexterStorey@users.noreply.github.com>
Co-authored-by: tedspare <ted.spare@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Monto <138862352+montocoder@users.noreply.github.com>
Co-authored-by: mohammed gehad <mohammed.gehad.1998@gmail.com>
Co-authored-by: Monto <138862352+monto7926@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Abhijeet Singh <asingh9829@gmail.com>
Co-authored-by: Kamil B. Demirci <kamil.demirci@indyaner.ch>
Co-authored-by: Denzil Samuel <71846487+samueldenzil@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Sahil Padvi <71093044+Sahil25061999@users.noreply.github.com>
Co-authored-by: Patel Divyesh <pateldivyesh1323@gmail.com>
Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: Mehul <mehulzr@gmail.com>
2023-09-14 09:45:13 +01:00
alannnc 605e744a58 fix: remove avatar form user select when loading insights (#11353) 2023-09-14 09:37:41 +02:00
alannnc aab0d6b18a fix: paypal save event type and payment (#11181)
* fix-paypal-save

* change type to use appId instead

* Remove duplicated code
2023-09-14 00:29:03 -07:00
WesleyandHariom Balhara dfde17111c test: verify embed helpers (#11318)
* test: add test cases to `window.getEmbedNamespace`

* test: add test cases when window.name doesn't have cal-embed=

* test: add test case to `getembedTheme` returning the theme from embedStore

* test: add test to `getEmbedTheme` returning the theme from query params

* Small refactor to reduce the time taken by tests and fix missing typings for embed functions on window

* Fix weird test failure

* Fix type import error caught by api.test.test

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-09-14 07:14:54 +00:00
Hariom Balhara 2591ad12df chore: Enable prettier on vitest files without causing test failures (#11190) 2023-09-14 08:58:08 +02:00
Kevin Crimiandalannnc 3a05ae6f74 feat: Add more metadata to Stripe transaction (#11221)
* Add booker info, event type title, and organizer info into Stripe metadata

* Add back bookingTitle in addition to eventTitle

* Remove .tool-versions

* Fix typo

---------

Co-authored-by: alannnc <alannnc@gmail.com>
2023-09-14 04:10:42 +00:00
efcfd7bb9b fix: when creating new user, set email handle as username (#11238)
* When creating new user, set email handle as username

* Accounting for external invited users

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-09-13 20:46:41 -03:00
Leo Giovanetti bce62829b7 fix: org shell and org user settings avatars + org owner username (#11342) 2023-09-13 18:04:36 +00:00
Carina WollendorferandCarinaWolli f320e486a2 fix issue with duplicate children event types (#11337)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-13 17:04:14 +00:00
MeenuyD 8c96e04b51 fix: insights upgrade screen has no title and subtitle for upgrade screen (#11328) 2023-09-13 15:41:53 +02:00
9eb3698ea5 fix: multiple organizer address bug (#11299)
* fix: multiple organizer address bug

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

* fix: add inPerson condition

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

* fix: change options

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

* Send _type along with the response to be able to determine what input was it. We can use that later

* Revert "Send _type along with the response to be able to determine what input was it. We can use that later"

This reverts commit c75ba57ad14b07d629b218d6e3eaede93b99ebaa.

* Update multiple organizer address test to verify the test fix

* Add a test to ensure that sending label would correctly add that label as location

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-09-13 04:19:01 +00:00
sean-brydonandPeer Richelsen 8ff07807f9 Fix overflow (#11320)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-12 21:46:18 +00:00
Crowdin Bot 9c7f655329 New Crowdin translations by Github Action 2023-09-12 21:29:30 +00:00
Alex van Andel 5c72fca816 Revert "fix: improve UX of team invite by copy link (#11009)"
This reverts commit 0150041496.
2023-09-12 22:26:22 +01:00
Alex van Andel aeafa3ef0a v3.2.10 2023-09-12 18:17:53 +01:00
Joe Au-YeungandAlex van Andel 3d4a3c7a93 fix: Connect selected calendar to credential (#11283)
release.yml / release (push) Canceled after 0s
* Reduce the unhandled promise

* Connect selected calendar to credential

* Write credential to selected calendar

* Type fix

* Handle catching promises in separate threads

* Only call to list calendars if there are no credentialIds

* Changed type of IntegrationCalendar from undef to null

* Adding missing property to getting started

* Also add calendars.tsx

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-09-12 18:09:05 +01:00
alannnc 1c1625d3fc fix: org admin delete other teams (#11247)
* fix org-admin delete other teams

* Fix delete from teams to delete from orgs endpoint
2023-09-12 16:19:17 +00:00
alannnc 75f9f045b8 fix: payments on round-robin events (#11248)
* Fix condition when no users are found for event type

* fix for types
2023-09-12 08:59:32 -07:00
Leo Giovanetti 86af383f6c feat: autolink org on provider signup (#11267)
* feat: autolink org on provider signup

* Adding env var in example

* slugifying inferred org username

* Updated org README

* Optional env var for autolinking added to readme

* Typo

* md syntax correction

* Verifying user when autolinked

* Also verifying users invited to org

* Applying feedback

* Scoping to Google for now

* Updating comment

* Updating readme with Google scoping
2023-09-12 15:17:34 +00:00
Keith WilliamsandAlex van Andel 84408025ed perf: pre-load booking data for all users (#11243)
* perf: pre-load booking data for all users

* Fixing property issues

* Fixes

* Removed unwanted console.log

* Made comment for intention of refactor more clear

* fix: types

* _count is optional

* Assign to const bookings

* Added mock for prisma booking

* Fixed unit tests

* Added lint ignore for prisma field

* Update bookingScenario.ts

* Fix linting

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-09-12 14:12:25 +01:00
Hariom Balhara 9fba8c548b fix: Prefill not working if firstAndLastName variant enabled in name field and name doesn't have any space (#11289) 2023-09-12 13:00:14 +02:00
sean-brydon 56f3647ec2 Fix max w (#11294) 2023-09-12 09:59:40 +00:00
GitStart-Cal.comandgitstart-calcom a5cb3ce573 feat: Create VerifyCodeDialog storybook file (#11286)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-09-12 00:09:13 +02:00
Udit Takkar 7581f4d641 fix: add inline color in og image (#11280) 2023-09-12 00:08:27 +02:00
Crowdin Bot a1d67b256e New Crowdin translations by Github Action 2023-09-11 18:41:56 +00:00
Joe Au-Yeung 75f9dad16e fix: Selected calendar unhandled promise when removing calendars (#11281) 2023-09-11 14:39:27 -04:00
sean-brydonandLeo Giovanetti 0a0d212373 fix: Handle removing user from org (#11227)
* Handle removing user from org

* Address no password or username being set

---------

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-09-11 14:46:38 +00:00
b8794ba807 fix: Fixed team image not showing on event-types page (#11231)
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-11 11:37:42 +00:00
Ahsan AhmadandPeer Richelsen 9b8e4ed385 fix: README typo uppercase ID (#11254)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-11 13:19:13 +02:00
Somay Chauhan 997b5d5290 fix: "no teams" text not visibl in organization's public url (#11257) 2023-09-11 13:10:36 +02:00
Yash d7080b41cb fix: text-emphasis removed (#11228) 2023-09-11 13:05:03 +02:00
Ujwal Kumar 32754bd120 fix: add hardcoded strings to locales so they can be translated (#11266) 2023-09-11 13:00:38 +02:00
Meet Mota 13fd02d174 fix: smaller date overwrite when no date is selected (#11250) 2023-09-11 12:49:27 +02:00
Crowdin Bot d6cbac54e0 New Crowdin translations by Github Action 2023-09-11 10:36:13 +00:00
Udit Takkar 489ffdf49a fix: hide book a team member (#11271) 2023-09-11 10:33:30 +00:00
Hariom BalharaandJoe Au-Yeung 37b22977ee fix: Booking and Cancellation related text changes (#10744)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-09-10 17:00:51 +00:00
Crowdin Bot 848a040162 New Crowdin translations by Github Action 2023-09-08 21:31:37 +00:00
Leo Giovanetti 350d84a047 fix: showing profile settings of other teams (#11245)
* fix: showing profile settings of other teams

* Updating URL
2023-09-08 18:29:06 -03:00
Shubham SinghandPeer Richelsen 9c3cbee363 feat: preference to show/hide available seats count in events (#11109)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-08 11:37:26 -04:00
Peer RichelsenandUdit Takkar 7e2ad3cea9 fix: inverted location logo (#11233)
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-09-08 15:59:05 +01:00
Alex van Andel 4ef52aa2c6 perf: Move query to get just the user email to the main get query (#11230) 2023-09-08 08:39:29 -03:00
Alex van Andel 3132c64205 perf: Improve getAggregatedAvailability by reducing reliance on Day.js (#11224) 2023-09-08 07:19:27 -03:00
Crowdin Bot f25f8e0909 New Crowdin translations by Github Action 2023-09-08 08:05:45 +00:00
Leo Giovanetti d8961f8eb9 fix: subteam invitation without teamId associated (#11222) 2023-09-08 09:02:12 +01:00
Crowdin Bot 9ff7cf650b New Crowdin translations by Github Action 2023-09-08 00:23:57 +00:00
ba4e717b59 chore: unify org data access (#11158)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-09-07 21:21:04 -03:00
Udit Takkar f326a9a620 fix: eventName variables (#10924) 2023-09-07 16:52:07 -04:00
Omar López 95be16c358 v3.2.9
release.yml / release (push) Canceled after 0s
2023-09-07 13:14:59 -07:00
Peer Richelsen 5621507f7e chore: fixed an i18n string (#11200) 2023-09-07 17:27:22 +00:00
Mehul bc6d58fd47 fix: bad setState call at datePicker component (#11175) 2023-09-07 17:11:59 +00:00
WesleyandCarinaWolli db29cc1251 feat: send new booking uid on reschedule webhook (#10654)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-07 16:27:46 +00:00
Patel Divyesh 69888c57b0 fix: Fixed SVG icons on light and dark mode (#11173) 2023-09-07 17:34:11 +02:00
5c0da23b97 feat: User Avatar and Org Logo (#10700)
* Pass organization name & logo

* Overflow hidden

* Show org icon on public page

* Add org logo to large user avatars

* Clean up

* Add org name and logo to context

* Get org logo from /avatar.png endpoint

* Do not query for logo

* Remove name and logo from session middleware

* Type fix

* Set user onboarding org logo

* feat: organization avatar component (#10788)

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

* Type fixes

* Type fix

* Transition to org slug for organization avatar

* Address feedback

* Clean up

* Clean up

* Type fix

* fix: set avatar cache control (#11163)

* test: Integration tests for handleNewBooking (#11044)

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

* fix: booking_paid webhook and added new payment metadata (#11093)

* app store improvements, logos, dark mode, added screenshots, fixed author names (#11164)

* fix: mobile event types and avatars (#11184)

* New Crowdin translations by Github Action

* fix: updateProfile metadata overwrite (#11188)

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

* New Crowdin translations by Github Action

---------

Co-authored-by: Sean Brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-09-07 17:26:40 +02:00
Hariom BalharaandCarina Wollendorfer 5324ec8051 fix: Routing Form - Show responses in the order of fields (#11143)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-09-07 15:22:39 +00:00
Crowdin Bot b946f89ffb New Crowdin translations by Github Action 2023-09-07 12:33:00 +00:00
Crowdin Bot 3e3581fabc New Crowdin translations by Github Action 2023-09-07 12:29:44 +00:00
Crowdin Bot 05ac339233 New Crowdin translations by Github Action 2023-09-07 12:26:19 +00:00
Crowdin Bot ef4675ff44 New Crowdin translations by Github Action 2023-09-07 12:22:42 +00:00
sean-brydon 47dc85ee1b chore: fixing darkmode data table (#11195) 2023-09-07 14:18:52 +02:00
Crowdin Bot 50342e33ed New Crowdin translations by Github Action 2023-09-06 23:43:28 +00:00
Omar Lópezandalannnc eebcb636f1 fix: updateProfile metadata overwrite (#11188)
Co-authored-by: alannnc <alannnc@gmail.com>
2023-09-06 16:40:50 -07:00
Crowdin Bot ed2ced899a New Crowdin translations by Github Action 2023-09-06 21:43:37 +00:00
Leo Giovanetti b7ae8c9594 fix: mobile event types and avatars (#11184) 2023-09-06 23:41:03 +02:00
Peer Richelsen fce98ebb13 app store improvements, logos, dark mode, added screenshots, fixed author names (#11164) 2023-09-07 01:17:04 +05:30
alannnc b81b221bf5 fix: booking_paid webhook and added new payment metadata (#11093) 2023-09-06 12:40:27 -07:00
f9eb335d0b test: Integration tests for handleNewBooking (#11044)
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-09-06 12:23:53 -07:00
Omar López d7d7bcd651 fix: set avatar cache control (#11163) 2023-09-06 12:22:13 -07:00
Crowdin Bot 4c861b9e67 New Crowdin translations by Github Action 2023-09-06 18:32:09 +00:00
Carina WollendorferandCarinaWolli 2fa704c39b fix: add make invite link to setup page (#11177)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-06 14:29:13 -04:00
Crowdin Bot 9ccdf21d75 New Crowdin translations by Github Action 2023-09-06 15:25:19 +00:00
GitStart-Cal.comandgitstart-calcom 9d6148da42 chore: add Wizard in storybook (CALCOM-10760) (#11166)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-09-06 17:22:02 +02:00
MehulandUdit Takkar bf437b7d6b fix: event duration can be 0 (#11145)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-09-06 17:21:24 +02:00
Udit Takkar 3701fcd68b fix: teams page error (#11171) 2023-09-06 11:38:21 +02:00
GitStart-Cal.comandgitstart-calcom 2f90dc7cb6 fix: DataTable storybook file is not rendering (#10984)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-09-06 09:18:06 +01:00
alannnc 829f90b82f chore: remove unused queries on user event types (#11159)
* remove unused queries on user event types

* Fix types
2023-09-06 05:00:06 -03:00
Crowdin Bot 3539693729 New Crowdin translations by Github Action 2023-09-06 05:11:51 +00:00
Joe Au-Yeung 868ba4396d fix: Installing Zoho CRM (#11161)
* Fix installing Zoho CRM

* Clean up
2023-09-06 05:08:59 +00:00
zomars d06e36a5cd Update yarn.lock 2023-09-05 14:46:14 -07:00
Crowdin Bot f61c9d9bbf New Crowdin translations by Github Action 2023-09-05 21:39:15 +00:00
Leo Giovanetti a2d521e001 fix: i18n issues reported by crowdin (#11160) 2023-09-05 18:36:17 -03:00
Omar López bf9c881d16 v3.2.8
release.yml / release (push) Canceled after 0s
2023-09-05 13:41:18 -07:00
alannnc 6dd0f9ce29 fix: selected organizer when creating event (#11157) 2023-09-05 13:38:59 -07:00
d34db7aef6 fix: Fix wrong footer Button in some languages (#10873)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-09-05 18:44:46 +00:00
Keith WilliamsandAlan b4b90f2d27 perf: Removed unused queries for user event types (#10568)
* perf: Removed unused queries for user event types

* Added empty children array

* Refactor to clean up

* Fixing type issue

* Removed extra call to get raw event types

* Temporarily putting children back

* Removing include from children

* Added check for children

* Put children back for testing

---------

Co-authored-by: Alan <alannnc@gmail.com>
2023-09-05 11:35:02 -07:00
Peer Richelsen 3f0380617a chore: removed gravatar (#11153) 2023-09-05 19:56:02 +02:00
Udit Takkar 7143923ea4 fix: rescheduling in require confirmation (#11152)
## What does this PR do?

Fixes https://github.com/calcom/cal.com/issues/11075

BEFORE:

Meeting has been rescheduled email and calendar event after submitting booking without confirming the meeting (require confirmation event type).

<img width="1086" alt="Screenshot 2023-09-05 at 7 45 44 PM" src="https://github.com/calcom/cal.com/assets/53316345/574c8d2f-3698-44ea-9466-aef162e09dd5">

AFTER:-

event scheduled email and calendar event created only after "awaiting approval" meeting has been confirmed.

<img width="1086" alt="Screenshot 2023-09-05 at 7 47 22 PM" src="https://github.com/calcom/cal.com/assets/53316345/af19ccea-d418-4a40-a9c6-88782522df53">


## Type of change

<!-- Please delete bullets that are not relevant. -->

- [x] Bug fix (non-breaking change which fixes an issue)

## How should this be tested?

1) Create a requires confirmation event type
2) open incognito tab or another browser and book a meeting (Attendee)
3) Confirm the booking (Organizer) and attendee would get meeting schedule email
4) Get the reschedule link from attendee's email and reschedule the booking

Both attendee and organiser shouldn't get booking email or calendar event  before organiser has accepted the meeting
2023-09-05 10:52:47 -07:00
DexterStorey dfccf4604f fix: better timezones and error handling in ai tools (#11085)
* current time in user timezone, let ai handle 401

* update agent.ts

* simple onboarding for non cal.com users

* reverting this line - unecessary

* v1.0.1
2023-09-05 13:56:26 +01:00
VijayandHariom Balhara c12abd75a8 fix: #10973 back button not working as expected in embed (#11073)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-09-05 15:40:09 +05:30
Crowdin Bot 2e12f09f05 New Crowdin translations by Github Action 2023-09-04 23:12:36 +00:00
alannnc 4258da1df5 remove double call on useSchedule() (#11141) 2023-09-04 23:10:19 +00:00
Hariom Balhara 41ef354c7b fix: FORM_SUBMITTED webhook payload change and support for Team Webhooks with it (#10986) 2023-09-04 17:04:57 -04:00
alannncandzomars 8891641953 hotfix: missing gCal calendarId (#11139)
Co-authored-by: zomars <zomars@me.com>
2023-09-04 20:25:39 +00:00
Udit Takkar 0150041496 fix: improve UX of team invite by copy link (#11009) 2023-09-04 12:43:37 -07:00
Carina WollendorferandCarinaWolli 13f9e97015 fix: variables for custom webhook payload (#11135)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-04 11:01:12 -07:00
Peer Richelsen 3c5901edb8 chore: cal.ai redirect (#11130) 2023-09-04 16:21:37 +00:00
Madhan KumarandAlan d26bab5880 fix: Require an 'Email' placeholder in the 'Add guest' field. (#11123)
* Adding_Email_PlaceHolder_Text

Signed-off-by: Madhan Kumar M <madhankumar26291@gmail.com>

* Adding_Email_PlaceHolder_Text

Signed-off-by: Madhan Kumar M <madhankumar26291@gmail.com>

* Adding_Email_PlaceHolder_Text

Signed-off-by: Madhan Kumar M <madhankumar26291@gmail.com>

* fixes

* fixes

---------

Signed-off-by: Madhan Kumar M <madhankumar26291@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
2023-09-04 09:13:12 -07:00
Udit Takkar 1c451a8d51 fix: installed badge (#11131) 2023-09-04 17:51:59 +02:00
Crowdin Bot 576261bf88 New Crowdin translations by Github Action 2023-09-04 15:50:49 +00:00
alannnc 7d944af7f6 fix: teams are listed on insights but then you cant find them on settings page 11087 cal 2401 (#11092)
* fix listing of other teams

* add more invalidate trpc queries

* Remove unused code

* Removed unnecesary flags
2023-09-04 08:47:53 -07:00
Crowdin Bot dfe7056c93 New Crowdin translations by Github Action 2023-09-04 14:55:48 +00:00
b27f01449a feat: make.com/integromat app (#8897)
* start make app integration

* setup integration

* add relevant env vars

* update app metadata

* import setup route in app setups

* fix typo

* add app store imports

* fix module import error

* update make readme

* move scheduler to app-store utils

* move add subscription to node scheduler

* move delete subscription to scheduler

* subscribe unsubscribe in zapier using common nodeScheduler

* fix lint errors

* revert settings.json

* update icon

* add app screenshots

* fix app description

* fix type errors

* update app code

* Delete .gitkeep

* delete unused template files

* get app invite link from env vars

* chore: handle error, cleanup readme, address review comments

* fix: update link in readme

* revert yarn.lock

* fix type errors

* Update packages/prisma/seed-app-store.ts

* Update .env.appStore.example

* Update .env.appStore.example

* update app readme

* fix param name in deleteSubcription

* fix listBookings handler

* Update turbo.json

* use default installation handler to install app

* use logger for console logs

* Fix inviteLink reading

* fix app setup handler

* Fix type issue

* always show app invite link

* fix type error

* add make invite link

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-04 14:52:34 +00:00
Udit Takkar 6dd77d0494 fix: create event type (#11046) 2023-09-04 15:02:30 +02:00
f73d38b632 fix: color tokens are not used in payment form (#10806)
Co-authored-by: VIneet Pradhan <138296762+vineetpradhan-dev@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-09-04 15:02:21 +02:00
Gaurav SutharandUdit Takkar a7d8f0c84e fix: add loading to delete event dialog (#11102)
* add loading to delete event dialog

* chore: use mutation isLoading

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-09-04 14:59:50 +02:00
Heath Henley b0164990c0 Conformation --> Confirmation (#11058) 2023-09-04 14:56:46 +02:00
Ikko Eltociear Ashimine bad72ba7bf chore: fix typo in embed-iframe.ts (#10858)
currenty -> currently
2023-09-04 17:48:42 +05:30
Crowdin Bot 1b317a98a4 New Crowdin translations by Github Action 2023-09-04 12:17:54 +00:00
Omar López 6a22d75caa fix: no-scrollbar for firefox (#10964) 2023-09-04 17:43:32 +05:30
a030861423 feat: recently added apps (#11118)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-04 10:30:21 +00:00
omahs bd9cd00c04 fix: typos (#11125) 2023-09-04 12:18:02 +02:00
Kushagra MathurandPeer Richelsen 6fd34af6f6 fix: dark mode icons for Riverside and Sirius Video (#11107)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-04 09:55:16 +00:00
1a85f336c5 fix: Booking Question Dialog: Dropdown covered by DialogFooter (CALCOM-10987) (#10989)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-04 11:42:10 +02:00
Peer Richelsen cc5096a54a chore: stop using telemetry for website (#11050) 2023-09-01 16:53:08 -07:00
alannnc eea7059226 Add test for different layouts (#11091) 2023-09-01 16:26:45 -07:00
Omar López 63f3247b1b fix: Vercel preview rewrites 404s (#11090) 2023-09-01 23:36:59 +02:00
Hariom BalharaandJoe Au-Yeung 7a769c38fb Remove duplicate fields (#11003)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-09-02 00:58:21 +05:30
alannnc 34c2565c70 fix paypal minor issues (#11069) 2023-09-01 11:03:27 -07:00
Crowdin Bot a130804f18 New Crowdin translations by Github Action 2023-09-01 17:55:12 +00:00
Leo Giovanetti 446e851065 fix: support subteam in org redirection (#11080) 2023-09-01 19:52:27 +02:00
Abhilash Sampath f06b4bfb9c fix: Add send sms description (#11063) 2023-09-01 19:12:10 +02:00
Alex van Andel 1896efa3a0 v3.2.7.1
release.yml / release (push) Canceled after 0s
2023-09-01 17:45:59 +01:00
Alex van Andel 62c8603863 fix: Revert #10366: Undoing then redo fix differently (#11078)
* Revert #10366: Undoing then redo fix differently

* Re-implement defaulting to first day in the month

* Update Embed.tsx and DateOverrideInputDialog
2023-09-01 17:12:01 +01:00
7d8559b0b0 chore: add Dropdown in storybook (#10874)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-01 18:08:34 +02:00
sean-brydon 7a48bbe553 Remove background colour (#11082) 2023-09-01 15:55:38 +00:00
nidhi sharma 817e95c246 fix #11077 space added on team events (#11081) 2023-09-01 15:30:37 +00:00
Udit Takkar 4ac6225a35 fix: unauthorized error in organization (#11039)
* fix: unauthorized error in organization

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

* chore

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

* fix: allow instance admin to update

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-09-01 14:02:58 +00:00
sean-brydon 38d9b93dee Fix avatar (#11076) 2023-09-01 14:23:28 +01:00
sean-brydonandUdit Takkar 376b38d4ed fix: Fixes username invite issue (#10998)
* Fixes username invite issue

* Ensure we only suggest email invites in org members

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-09-01 17:31:45 +05:30
Hariom Balhara 0e181f6d9f Revert "feat: adds next cold start profiler (#11014)" (#11072)
This reverts commit 05631d093f.
2023-09-01 10:18:17 +01:00
Omar López 4dbb692156 v3.2.7
release.yml / release (push) Canceled after 0s
2023-08-31 15:50:53 -07:00
alannnc 0068718d24 fix sub quantity update stripe (#11057) 2023-08-31 22:46:48 +00:00
alannnc ac817b3542 easy fix (#11054) 2023-08-31 22:44:35 +00:00
zomars 97f2cffd1f fix: zod utils due to upgrade 2023-08-31 15:27:51 -07:00
zomars b6dea9c161 Sync packages 2023-08-31 12:57:59 -07:00
57eb4746ad chore: add Switch in storybook (CALCOM-10760) (#10804)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-31 21:36:30 +02:00
eff9c87964 chore: add Timezone Select in storybook (CALCOM-10760) (#10966)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-31 21:34:22 +02:00
e06c07bfec fix: email embed – remove collapsible and permanently show times (#10996)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Mehul <mehulzr@gmail.com>
2023-08-31 19:32:52 +00:00
GitStart-Cal.comandgitstart-calcom 332c00dac2 fix: Fix tooltip control on ButtonPlayground storybook file (fix-ButtonTooltip) (#10937)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-31 21:17:59 +02:00
GitStart-Cal.comandgitstart-calcom e3523db6bb fix: Add controls for Select Field storybook file (#10936)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-31 21:17:09 +02:00
GitStart-Cal.comandgitstart-calcom 9c0f046b2a fix: Logo storybook file with invalid icons (fix-logo) (#11018)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-31 21:16:21 +02:00
alannnc 6e1a717ecf fix: add metadata to stripe payment intent (#11053) 2023-08-31 19:15:45 +00:00
b2e098c781 chore: add ToggleGroup in storybook (#10802)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-08-31 21:12:13 +02:00
Leo GiovanettiandPeer Richelsen 3010900358 fix: admin org list without members (#11051)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-31 19:11:28 +00:00
GitStart-Cal.comandgitstart-calcom 9b043f44a0 fix: List storybook file is empty (fix-list) (#10965)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-31 21:11:01 +02:00
DexterStorey 814674fe40 resolve zod versions across child packages (#11052) 2023-08-31 20:10:59 +01:00
acaa79b0b8 feat: sorting for workflow and routing forms (#10780)
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-31 18:57:33 +00:00
GitStart-Cal.comandgitstart-calcom 781a0f4a7c chore: add ErrorBoundary in storybook (CALCOM-10760) (#10872)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-31 20:56:50 +02:00
Patel Divyesh b2028870d0 fix: broken company profile link on team booking page (#10978) 2023-08-31 20:45:26 +02:00
8f70be8861 fix: Error when running storybook (#11037)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-31 20:35:43 +02:00
alannnc 53c8f8c04a fix: include app data and credentials from DB (#11048)
* include app data and credentials from DB

* Improve performance
2023-08-31 11:10:58 -07:00
alannnc c4e5d168a5 fix: handle collective multiple host on destinationCalendar (#10967) 2023-08-31 10:47:02 -07:00
sean-brydon 500bfb4d82 chore: sheet darkmode and improve responsive (#11047) 2023-08-31 19:09:06 +02:00
Sahil Padvi 024b92b9ce styles:dark mode color fix (#11004) 2023-08-31 17:18:27 +01:00
Crowdin Bot f23b63cc98 New Crowdin translations by Github Action 2023-08-31 14:24:31 +00:00
Hariom Balhara edc90e3f27 Handle disabling of ORGANIZATIONS_ENABLED flag (#11041) 2023-08-31 19:48:20 +05:30
Omar López 05631d093f feat: adds next cold start profiler (#11014) 2023-08-31 12:25:52 +02:00
GitStart-Cal.comandgitstart-calcom 6000ff4b48 chore: add ColorPicker in storybook (CALCOM-10760) (#10866)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-31 12:16:37 +02:00
Anik Dhabal Babu cd7de98e3f fix: Set mobile availability (#11027) 2023-08-31 12:08:34 +02:00
3f51858d27 chore: add Popover in storybook (#11021)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-31 12:04:09 +02:00
Denzil SamuelandUdit Takkar 2c498ace5c fix: fix-tablet-menu-not-centered-sidebar (#11020)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-08-31 12:03:32 +02:00
Kamil B. Demirci 595d781d51 refactor: improvements on german translation (#10898) 2023-08-31 12:03:11 +02:00
Udit Takkar 3c508568da fix: lower case slugs in teams (#11026)
* fix: lower case slugs in teams

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

* fix: use slugify

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-30 23:34:19 -07:00
Abhijeet Singhandalannnc 3204d0af34 feat: team admin: see connected apps of team members (#11036)
* added feature:team admin can see connected apps of members

* fixed the type error

* Update packages/lib/server/queries/teams/index.ts

* Minor fixes

---------

Co-authored-by: alannnc <alannnc@gmail.com>
2023-08-31 01:20:31 +00:00
25684f9040 fix: meeting ended trigger for webhooks and zapier sometimes not working (#10946)
Co-authored-by: mohammed gehad <mohammed.gehad.1998@gmail.com>
Co-authored-by: Monto <138862352+monto7926@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-30 23:17:42 +00:00
Crowdin Bot bc9aeef710 New Crowdin translations by Github Action 2023-08-30 23:13:45 +00:00
393411a47a feat: cal ai (#10992)
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Co-authored-by: tedspare <ted.spare@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-08-30 16:10:59 -07:00
Leo Giovanetti 356117feaf fix: other reported issues (#11015)
* fix: weird margin top in avatar

* fix: pending users are shown on booking page

* fix: avatar and naming issues

* fix: toast alignment and removing unneeded titles

* missing changes from toast improvements

* feat: empty state for teams without event types

* Removing console.log
2023-08-30 13:42:34 -07:00
Leo Giovanetti 1bf68d50e0 fix: multiple duration when booking (#11032) 2023-08-30 12:19:00 -07:00
Crowdin Bot 4a35e85756 New Crowdin translations by Github Action 2023-08-30 10:15:41 +00:00
Peer Richelsenandalannnc a4f56bf680 Delete add-PRs-to-project-reviewing-PRs.yml (#11008)
Co-authored-by: alannnc <alannnc@gmail.com>
2023-08-30 10:12:54 +00:00
nicktrnandPeer Richelsen a308075bc3 feat: 2fa backup codes (#10600)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-30 07:33:48 +00:00
efa6d464a3 refactor: removed redundant test (#10785)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-08-29 18:14:30 +00:00
Crowdin Bot 811262d7ee New Crowdin translations by Github Action 2023-08-29 17:29:54 +00:00
Udit Takkar 99a5dcf96a fix: type error in booker (#11011) 2023-08-29 10:27:06 -07:00
MehulandUdit Takkar 79f092673a fix: signup password hint validation (#10454)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-08-29 13:56:45 +02:00
b285f27d00 feat: option for adding ics events to workflow reminders (#10856)
Co-authored-by: SMLukwiya <sundaymorganl@gmail.com>
Co-authored-by: Monto <138862352+monto7926@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-29 13:56:26 +02:00
1fa87ae179 fix: Modal popup overflow issue in reschedule booking (#10976)
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-29 12:36:15 +02:00
Patel DivyeshandPeer Richelsen f6f4b67bf9 fix: Team Logo not Showing up in Team Event Create Modal (#10959)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-29 12:35:50 +02:00
MehulandHariom Balhara 8c8550e6f8 fix: switching between months leads to incorrect autoselect first available date (#10366)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-29 12:32:56 +02:00
Joe Au-YeungandHariom Balhara c6246c9408 refactor: Eventtype app context after orgs (#10781)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-29 11:32:20 +05:30
Omar López ccc814eb54 v3.2.6
release.yml / release (push) Canceled after 0s
2023-08-28 17:04:09 -07:00
Joe Au-Yeung 2ee0ec11d6 Install Stripe on team from team event type (#10920) 2023-08-28 16:39:57 -07:00
Syed Ali Shahbaz 3223beb9ac omit id as it is autocreated, should not be req (#10982) 2023-08-28 06:57:01 +00:00
nicktrnandzomars c1bcfcfa3d fix: replace all async foreach callbacks (#10157)
Co-authored-by: zomars <zomars@me.com>
2023-08-26 00:43:10 +00:00
83aea7d28c fix: webhooks for managed event types (#10958)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-08-26 00:27:05 +00:00
Alex van Andel a810a538e9 v3.2.5
release.yml / release (push) Canceled after 0s
2023-08-25 17:23:22 +01:00
sean-brydon 1cd0d7b5ca use team id :/ (#10957) 2023-08-25 16:14:28 +00:00
Alex van Andel 4540c79580 v3.2.4
release.yml / release (push) Canceled after 0s
2023-08-25 15:32:55 +01:00
Udit Takkar 00d01882d4 fix: private team regression (#10914)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-25 14:43:48 +01:00
Omar López 6ce4122528 fix: allows patching booking and period limits from API (#10935) 2023-08-25 14:29:04 +01:00
7ddbcfcdcf feat: Adds filter-by-email on /users/_get endpoint for admin calls (#10928)
* adds filter by email on users get for admins

* swagger update

* Update apps/api/pages/api/users/_get.ts

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

* .parse is required, also fixed some other issues

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-08-25 14:15:13 +01:00
b1b01ed383 remove close button from TopBanner (#10705)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-08-25 15:50:39 +03:00
6f5d9dc41f test: Create unit tests for react components in packages/ui/components/form/select (#10459)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-25 12:18:53 +00:00
GitStart-Cal.comandgitstart-calcom ac190b89dd chore: add UnpublishedEntity in storybook (CALCOM-10760) (#10864)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-25 15:12:19 +03:00
990441295c chore: add ImageUploader in storybook (CALCOM-10760) (#10865)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-25 15:06:55 +03:00
1e4c20073c chore: fix all @typescript-eslint/no-non-null-assertion warnings (#10635)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-25 14:56:20 +03:00
80353d67a0 chore: fix all @typescript-eslint/no-unused-vars warnings (#10645)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-25 11:48:24 +00:00
Keith Williams d55b4b051a perf: Fix slowest db query (again) (#10954) 2023-08-25 14:27:37 +03:00
Hariom Balhara 25d90e3e12 test: Upcoming bookings sorting order (#10950)
* Fix order by for bookings array
Fix orderBy for bookings

* Test

* Text change

* Add comments
2023-08-25 12:55:41 +03:00
Hariom Balhara 1727eb5625 fix: Unordered bookings (#10943) 2023-08-25 15:06:57 +05:30
sean-brydon 58f4224c66 Fix invite flow (#10951) 2023-08-25 09:19:03 +00:00
Udit Takkar 5d491e0a12 fix: unauthorized after email change (#10821) 2023-08-24 16:47:34 -07:00
alannncandOmar López 1eef5a56c3 fix: include orgs slug in getUserByUsername just like [user]/[type] page (#10723)
Co-authored-by: Omar López <zomars@me.com>
2023-08-24 16:41:06 -07:00
d6740503be feat: Column View Available Slots Fix (#9815)
Co-authored-by: Vinoth Kumar V <vinoth_kumar_v@Vinoths-MacBook-Pro.local>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-08-24 16:39:45 -07:00
David Oksman 9f647ead46 fix: missing timezones in dropdown list (#10823) 2023-08-24 13:57:11 -07:00
Thibault Mathian f480afcace fix: caching and fetching of feature flags (#10837) 2023-08-24 13:43:52 -07:00
Hariom Balhara 8d73d15b8a perf: Lazy load framer motion stuff as much as we can (#10876) 2023-08-24 13:40:01 -07:00
Hariom Balhara e8370ef950 perf: Remove non booker apps from booker page (#10905) 2023-08-24 13:38:15 -07:00
Hariom Balhara e859c8dc58 test: Add attendee phone number test (#10932) 2023-08-24 11:21:49 -07:00
637 changed files with 20242 additions and 7444 deletions
+32
View File
@@ -213,6 +213,10 @@ NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
# use organizations
ORGANIZATIONS_ENABLED=
# This variable should only be set to 1 or true if you want to autolink external provider sing-ups with
# existing organizations based on email domain address
ORGANIZATIONS_AUTOLINK=
# Vercel Config to create subdomains for organizations
# Get it from https://vercel.com/<TEAM_OR_USER_NAME>/<PROJECT_SLUG>/settings
PROJECT_ID_VERCEL=
@@ -225,3 +229,31 @@ AUTH_BEARER_TOKEN_VERCEL=
# Used for E2E tests on Apple Calendar
E2E_TEST_APPLE_CALENDAR_EMAIL=""
E2E_TEST_APPLE_CALENDAR_PASSWORD=""
# - APP CREDENTIAL SYNC ***********************************************************************************
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
# You can use: `openssl rand -base64 32` to generate one
CALCOM_WEBHOOK_SECRET=""
# This is the header name that will be used to verify the webhook secret. Should be in lowercase
CALCOM_WEBHOOK_HEADER_NAME="calcom-webhook-secret"
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
# Key should match on Cal.com and your application
# must be 32 bytes for AES256 encryption algorithm
# You can use: `openssl rand -base64 24` to generate one
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=""
# - OIDC E2E TEST *******************************************************************************************
# Ensure this ADMIN EMAIL is present in the SAML_ADMINS list
E2E_TEST_SAML_ADMIN_EMAIL=
E2E_TEST_SAML_ADMIN_PASSWORD=
E2E_TEST_OIDC_CLIENT_ID=
E2E_TEST_OIDC_CLIENT_SECRET=
E2E_TEST_OIDC_PROVIDER_DOMAIN=
E2E_TEST_OIDC_USER_EMAIL=
E2E_TEST_OIDC_USER_PASSWORD=
# ***********************************************************************************************************
@@ -1,16 +0,0 @@
name: Add PRs to project Reviewing PRs
on:
pull_request:
types:
- opened
jobs:
add-PR-to-project:
name: Add PRs to project Reviewing PRs
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.1.0
with:
project-url: https://github.com/orgs/calcom/projects/11
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
@@ -0,0 +1,33 @@
name: Cron - monthlyDigestEmail
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs on the 28th, 29th, 30th and 31st of every month (see https://crontab.guru)
- cron: "59 23 28-31 * *"
jobs:
cron-monthlyDigestEmail:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: Check if today is the last day of the month
id: check-last-day
run: |
LAST_DAY=$(date -d tomorrow +%d)
if [ "$LAST_DAY" == "01" ]; then
echo "::set-output name=is_last_day::true"
else
echo "::set-output name=is_last_day::false"
fi
- name: cURL request
if: ${{ env.APP_URL && env.CRON_API_KEY && steps.check-last-day.outputs.is_last_day == 'true' }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/monthlyDigestEmail \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
@@ -0,0 +1,23 @@
name: Cron - webhookTriggers
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “every 5 minutes” (see https://crontab.guru)
- cron: "*/5 * * * *"
jobs:
cron-webhookTriggers:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_API_KEY }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/webhookTriggers \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: crowdin action
uses: crowdin/github-action@1.5.1
uses: crowdin/github-action@v1.13.0
with:
upload_sources: true
upload_translations: true
+18
View File
@@ -0,0 +1,18 @@
name: Auto Comment Merge Conflicts
on: push
permissions:
pull-requests: write
jobs:
auto-comment-merge-conflicts:
runs-on: ubuntu-latest
steps:
- uses: codytseng/auto-comment-merge-conflicts@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
comment-body: "Hey there, there is a merge conflict, can you take a look?"
wait-ms: 3000
max-retries: 5
label-name: "🚨 merge conflict"
ignore-authors: dependabot,otherAuthor
-1
View File
@@ -7,7 +7,6 @@ public
*.lock
*.log
*.test.ts
.gitignore
.npmignore
+5 -5
View File
@@ -1,6 +1,6 @@
# Contributing to Cal.com
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
Contributions are what makes the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
@@ -37,7 +37,7 @@ Contributions are what make the open source community such an amazing place to l
</tr>
<tr>
<td>
Core Features (Booking page, availabilty, timezone calculation)
Core Features (Booking page, availability, timezone calculation)
</td>
<td>
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:%22High+priority%22">
@@ -132,7 +132,7 @@ If you get errors, be sure to fix them before committing.
## Making a Pull Request
- Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating you PR.
- If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. See more about [Linking a pull request to an issue
](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
- Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating your PR.
- If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. See more about [Linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
- Be sure to fill the PR Template accordingly.
- Review [App Contribution Guidelines](./packages/app-store/CONTRIBUTING.md) when building integrations
+4 -4
View File
@@ -423,7 +423,7 @@ yarn seed-app-store
```
You will need to complete a few more steps to activate Google Calendar App.
Make sure to complete section "Obtaining the Google API Credentials". After the do the
Make sure to complete section "Obtaining the Google API Credentials". After that do the
following
1. Add extra redirect URL `<Cal.com URL>/api/auth/callback/google`
@@ -449,8 +449,8 @@ following
7. Click "Create".
8. Now copy the Client ID and Client Secret to your `.env` file into the `ZOOM_CLIENT_ID` and `ZOOM_CLIENT_SECRET` fields.
9. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zoomvideo/callback` replacing Cal.com URL with the URI at which your application runs.
10. Also add the redirect URL given above as a allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form.
11. You don't need to provide basic information about your app. Instead click at "Scopes" and then at "+ Add Scopes". On the left, click the category "Meeting" and check the scope `meeting:write`.
10. Also add the redirect URL given above as an allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form.
11. You don't need to provide basic information about your app. Instead click on "Scopes" and then on "+ Add Scopes". On the left, click the category "Meeting" and check the scope `meeting:write`.
12. Click "Done".
13. You're good to go. Now you can easily add your Zoom integration in the Cal.com settings.
@@ -526,7 +526,7 @@ For example, `Cal.com (support@cal.com)`.
3. Copy Account SID to your `.env` file into the `TWILIO_SID` field
4. Copy Auth Token to your `.env` file into the `TWILIO_TOKEN` field
5. Copy your Twilio phone number to your `.env` file into the `TWILIO_PHONE_NUMBER` field
6. Add your own sender id to the `.env` file into the `NEXT_PUBLIC_SENDER_ID` field (fallback is Cal.com)
6. Add your own sender ID to the `.env` file into the `NEXT_PUBLIC_SENDER_ID` field (fallback is Cal.com)
7. Create a messaging service (Develop -> Messaging -> Services)
8. Choose any name for the messaging service
9. Click 'Add Senders'
+15
View File
@@ -0,0 +1,15 @@
BACKEND_URL=http://localhost:3002/api
APP_ID=cal-ai
APP_URL=http://localhost:3000/apps/cal-ai
# Used to verify requests from sendgrid. You can generate a new one with: `openssl rand -hex 32`
PARSE_KEY=
OPENAI_API_KEY=
# Optionally trace completions at https://smith.langchain.com
# LANGCHAIN_TRACING_V2=true
# LANGCHAIN_ENDPOINT=
# LANGCHAIN_API_KEY=
# LANGCHAIN_PROJECT=
+48
View File
@@ -0,0 +1,48 @@
# Cal.com Email Assistant
Welcome to the first stage of Cal AI!
This app lets you chat with your calendar via email:
- Turn informal emails into bookings eg. forward "wanna meet tmrw at 2pm?"
- List and rearrange your bookings eg. "Cancel my next meeting"
- Answer basic questions about your busiest times eg. "How does my Tuesday look?"
The core logic is contained in [agent/route.ts](/apps/ai/src/app/api/agent/route.ts). Here, a [LangChain Agent Executor](https://docs.langchain.com/docs/components/agents/agent-executor) is tasked with following your instructions. Given your last-known timezone, working hours, and busy times, it attempts to CRUD your bookings.
_The AI agent can only choose from a set of tools, without ever seeing your API key._
Emails are cleaned and routed in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts) using [MailParser](https://nodemailer.com/extras/mailparser/).
Incoming emails are routed by email address. Addresses are verified by [DKIM record](https://support.google.com/a/answer/174124?hl=en), making it hard to spoof them.
## Getting Started
### Development
If you haven't yet, please run the [root setup](/README.md) steps.
Before running the app, please see [env.mjs](./src/env.mjs) for all required environment variables. You'll need:
- An [OpenAI API key](https://platform.openai.com/account/api-keys) with access to GPT-4
- A [SendGrid API key](https://app.sendgrid.com/settings/api_keys)
- A default sender email (for example, `ai@cal.dev`)
- The Cal AI's app ID and URL (see [add.ts](/packages/app-store/cal-ai/api/index.ts))
To stand up the API and AI apps simultaneously, simply run `yarn dev:ai`.
### Email Router
To expose the AI app, run `ngrok http 3000` (or the AI app's port number) in a new terminal. You may need to install [nGrok](https://ngrok.com/).
To forward incoming emails to the Node.js server, one option is to use [SendGrid's Inbound Parse Webhook](https://docs.sendgrid.com/for-developers/parsing-email/setting-up-the-inbound-parse-webhook).
1. [Sign up for an account](https://signup.sendgrid.com/)
2. Go to Settings > [Inbound Parse](https://app.sendgrid.com/settings/parse) > Add Host & URL.
3. For subdomain, use `<sub>.<domain>.com` for now, where `sub` can be any subdomain but `domain.com` will need to be verified via MX records in your environment variables, eg. on [Vercel](https://vercel.com/guides/how-to-add-vercel-environment-variables).
4. Use the nGrok URL from above as the **Destination URL**.
5. Activate "POST the raw, full MIME message".
6. Send an email to `<anyone>@ai.example.com`. You should see a ping on the nGrok listener and Node.js server.
7. Adjust the logic in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts), save to hot-reload, and send another email to test the behaviour.
Please feel free to improve any part of this architecture.
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+24
View File
@@ -0,0 +1,24 @@
const withBundleAnalyzer = require("@next/bundle-analyzer");
const plugins = [];
plugins.push(withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
/** @type {import("next").NextConfig} */
const nextConfig = {
async redirects() {
return [
{
source: "/",
destination: "https://cal.com/ai",
permanent: true,
},
];
},
i18n: {
defaultLocale: "en",
locales: ["en"],
},
reactStrictMode: true,
};
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
+26
View File
@@ -0,0 +1,26 @@
{
"name": "@calcom/ai",
"version": "1.0.1",
"private": true,
"author": "Cal.com Inc.",
"dependencies": {
"@calcom/prisma": "*",
"@t3-oss/env-nextjs": "^0.6.1",
"langchain": "^0.0.131",
"mailparser": "^3.6.5",
"next": "^13.4.6",
"supports-color": "8.1.1",
"zod": "^3.22.2"
},
"devDependencies": {
"@types/mailparser": "^3.4.0"
},
"scripts": {
"build": "next build",
"dev": "next dev -p 3005",
"format": "npx prettier . --write",
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"start": "next start"
}
}
+45
View File
@@ -0,0 +1,45 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import agent from "../../../utils/agent";
import sendEmail from "../../../utils/sendEmail";
import { verifyParseKey } from "../../../utils/verifyParseKey";
/**
* Launches a LangChain agent to process an incoming email,
* then sends the response to the user.
*/
export const POST = async (request: NextRequest) => {
const verified = verifyParseKey(request.url);
if (!verified) {
return new NextResponse("Unauthorized", { status: 401 });
}
const json = await request.json();
const { apiKey, userId, message, subject, user, replyTo } = json;
if ((!message && !subject) || !user) {
return new NextResponse("Missing fields", { status: 400 });
}
try {
const response = await agent(`${subject}\n\n${message}`, user, apiKey, userId);
// Send response to user
await sendEmail({
subject: `Re: ${subject}`,
text: response.replace(/(?:\r\n|\r|\n)/g, "\n"),
to: user.email,
from: replyTo,
});
return new NextResponse("ok");
} catch (error) {
return new NextResponse(
(error as Error).message || "Something went wrong. Please try again or reach out for help.",
{ status: 500 }
);
}
};
+154
View File
@@ -0,0 +1,154 @@
import type { ParsedMail, Source } from "mailparser";
import { simpleParser } from "mailparser";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import prisma from "@calcom/prisma";
import { env } from "../../../env.mjs";
import { fetchAvailability } from "../../../tools/getAvailability";
import { fetchEventTypes } from "../../../tools/getEventTypes";
import getHostFromHeaders from "../../../utils/host";
import now from "../../../utils/now";
import sendEmail from "../../../utils/sendEmail";
import { verifyParseKey } from "../../../utils/verifyParseKey";
/**
* Verifies email signature and app authorization,
* then hands off to booking agent.
*/
export const POST = async (request: NextRequest) => {
const verified = verifyParseKey(request.url);
if (!verified) {
return new NextResponse("Unauthorized", { status: 401 });
}
const formData = await request.formData();
const body = Object.fromEntries(formData);
// body.dkim looks like {@domain-com.22222222.gappssmtp.com : pass}
const signature = (body.dkim as string).includes(" : pass");
const envelope = JSON.parse(body.envelope as string);
const aiEmail = envelope.to[0];
// Parse email from mixed MIME type
const parsed: ParsedMail = await simpleParser(body.email as Source);
if (!parsed.text && !parsed.subject) {
return new NextResponse("Email missing text and subject", { status: 400 });
}
const user = await prisma.user.findUnique({
select: {
email: true,
id: true,
timeZone: true,
credentials: {
select: {
appId: true,
key: true,
},
},
},
where: { email: envelope.from },
});
// User is not a cal.com user or is using an unverified email.
if (!signature || !user) {
await sendEmail({
html: `Thanks for your interest in Cal AI! To get started, Make sure you have a <a href="https://cal.com/signup" target="_blank">cal.com</a> account with this email address.`,
subject: `Re: ${body.subject}`,
text: `Thanks for your interest in Cal AI! To get started, Make sure you have a cal.com account with this email address. You can sign up for an account at: https://cal.com/signup`,
to: envelope.from,
from: aiEmail,
});
return new NextResponse("ok");
}
const credential = user.credentials.find((c) => c.appId === env.APP_ID)?.key;
// User has not installed the app from the app store. Direct them to install it.
if (!(credential as { apiKey: string })?.apiKey) {
const url = env.APP_URL;
await sendEmail({
html: `Thanks for using Cal AI! To get started, the app must be installed. <a href=${url} target="_blank">Click this link</a> to install it.`,
subject: `Re: ${body.subject}`,
text: `Thanks for using Cal AI! To get started, the app must be installed. Click this link to install the Cal AI app: ${url}`,
to: envelope.from,
from: aiEmail,
});
return new NextResponse("ok");
}
const { apiKey } = credential as { apiKey: string };
// Pre-fetch data relevant to most bookings.
const [eventTypes, availability] = await Promise.all([
fetchEventTypes({
apiKey,
}),
fetchAvailability({
apiKey,
userId: user.id,
dateFrom: now(user.timeZone),
dateTo: now(user.timeZone),
}),
]);
if ("error" in availability) {
await sendEmail({
subject: `Re: ${body.subject}`,
text: "Sorry, there was an error fetching your availability. Please try again.",
to: user.email,
from: aiEmail,
});
console.error(availability.error);
return new NextResponse("Error fetching availability. Please try again.", { status: 400 });
}
if ("error" in eventTypes) {
await sendEmail({
subject: `Re: ${body.subject}`,
text: "Sorry, there was an error fetching your event types. Please try again.",
to: user.email,
from: aiEmail,
});
console.error(eventTypes.error);
return new NextResponse("Error fetching event types. Please try again.", { status: 400 });
}
const { workingHours } = availability;
const appHost = getHostFromHeaders(request.headers);
// Hand off to long-running agent endpoint to handle the email. (don't await)
fetch(`${appHost}/api/agent?parseKey=${env.PARSE_KEY}`, {
body: JSON.stringify({
apiKey,
userId: user.id,
message: parsed.text,
subject: parsed.subject,
replyTo: aiEmail,
user: {
email: user.email,
eventTypes,
timeZone: user.timeZone,
workingHours,
},
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
await new Promise((r) => setTimeout(r, 1000));
return new NextResponse("ok");
};
+43
View File
@@ -0,0 +1,43 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
/**
* Specify your client-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars. To expose them to the client, prefix them with
* `NEXT_PUBLIC_`.
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
},
/**
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
* middlewares) or client-side so we need to destruct manually.
*/
runtimeEnv: {
BACKEND_URL: process.env.BACKEND_URL,
APP_ID: process.env.APP_ID,
APP_URL: process.env.APP_URL,
PARSE_KEY: process.env.PARSE_KEY,
NODE_ENV: process.env.NODE_ENV,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
DATABASE_URL: process.env.DATABASE_URL,
},
/**
* Specify your server-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars.
*/
server: {
BACKEND_URL: z.string().url(),
APP_ID: z.string().min(1),
APP_URL: z.string().url(),
PARSE_KEY: z.string().min(1),
NODE_ENV: z.enum(["development", "test", "production"]),
OPENAI_API_KEY: z.string().min(1),
SENDGRID_API_KEY: z.string().min(1),
DATABASE_URL: z.string().url(),
},
});
@@ -0,0 +1,110 @@
import { DynamicStructuredTool } from "langchain/tools";
import { z } from "zod";
import { env } from "../env.mjs";
/**
* Creates a booking for a user by event type, times, and timezone.
*/
const createBooking = async ({
apiKey,
userId,
eventTypeId,
start,
end,
timeZone,
language,
responses,
}: {
apiKey: string;
userId: number;
eventTypeId: number;
start: string;
end: string;
timeZone: string;
language: string;
responses: { name?: string; email?: string; location?: string };
title?: string;
status?: string;
}): Promise<string | Error | { error: string }> => {
const params = {
apiKey,
userId: userId.toString(),
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/bookings?${urlParams.toString()}`;
const response = await fetch(url, {
body: JSON.stringify({
end,
eventTypeId,
language,
metadata: {},
responses,
start,
timeZone,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
// Let GPT handle this. This will happen when wrong event type id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return {
error: data.message,
};
}
return "Booking created";
};
const createBookingTool = (apiKey: string, userId: number) => {
return new DynamicStructuredTool({
description:
"Tries to create a booking. If the user is unavailable, it will return availability that day, allowing you to avoid the getAvailability step in many cases.",
func: async ({ eventTypeId, start, end, timeZone, language, responses, title, status }) => {
return JSON.stringify(
await createBooking({
apiKey,
userId,
end,
eventTypeId,
language,
responses,
start,
status,
timeZone,
title,
})
);
},
name: "createBookingIfAvailable",
schema: z.object({
end: z
.string()
.describe("This should correspond to the event type's length, unless otherwise specified."),
eventTypeId: z.number(),
language: z.string(),
responses: z
.object({
email: z.string().optional(),
name: z.string().optional(),
})
.describe("External invited user. Not the user making the request."),
start: z.string(),
status: z.string().optional().describe("ACCEPTED, PENDING, CANCELLED or REJECTED"),
timeZone: z.string(),
title: z.string().optional(),
}),
});
};
export default createBookingTool;
+66
View File
@@ -0,0 +1,66 @@
import { DynamicStructuredTool } from "langchain/tools";
import { z } from "zod";
import { env } from "../env.mjs";
/**
* Cancels a booking for a user by ID with reason.
*/
const cancelBooking = async ({
apiKey,
id,
reason,
}: {
apiKey: string;
id: string;
reason: string;
}): Promise<string | { error: string }> => {
const params = {
apiKey,
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/bookings/${id}/cancel?${urlParams.toString()}`;
const response = await fetch(url, {
body: JSON.stringify({ reason }),
headers: {
"Content-Type": "application/json",
},
method: "DELETE",
});
// Let GPT handle this. This will happen when wrong booking id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
return "Booking cancelled";
};
const cancelBookingTool = (apiKey: string) => {
return new DynamicStructuredTool({
description: "Cancel a booking",
func: async ({ id, reason }) => {
return JSON.stringify(
await cancelBooking({
apiKey,
id,
reason,
})
);
},
name: "cancelBooking",
schema: z.object({
id: z.string(),
reason: z.string(),
}),
});
};
export default cancelBookingTool;
+82
View File
@@ -0,0 +1,82 @@
import { DynamicStructuredTool } from "langchain/tools";
import { z } from "zod";
import { env } from "../env.mjs";
import type { Availability } from "../types/availability";
/**
* Fetches availability for a user by date range and event type.
*/
export const fetchAvailability = async ({
apiKey,
userId,
dateFrom,
dateTo,
eventTypeId,
}: {
apiKey: string;
userId: number;
dateFrom: string;
dateTo: string;
eventTypeId?: number;
}): Promise<Partial<Availability> | { error: string }> => {
const params: { [k: string]: string } = {
apiKey,
userId: userId.toString(),
dateFrom,
dateTo,
};
if (eventTypeId) params["eventTypeId"] = eventTypeId.toString();
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/availability?${urlParams.toString()}`;
const response = await fetch(url);
if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
return {
busy: data.busy,
dateRanges: data.dateRanges,
timeZone: data.timeZone,
workingHours: data.workingHours,
};
};
const getAvailabilityTool = (apiKey: string, userId: number) => {
return new DynamicStructuredTool({
description: "Get availability within range.",
func: async ({ dateFrom, dateTo, eventTypeId }) => {
return JSON.stringify(
await fetchAvailability({
apiKey,
userId,
dateFrom,
dateTo,
eventTypeId,
})
);
},
name: "getAvailability",
schema: z.object({
dateFrom: z.string(),
dateTo: z.string(),
eventTypeId: z
.number()
.optional()
.describe(
"The ID of the event type to filter availability for if you've called getEventTypes, otherwise do not include."
),
}),
});
};
export default getAvailabilityTool;
+75
View File
@@ -0,0 +1,75 @@
import { DynamicStructuredTool } from "langchain/tools";
import { z } from "zod";
import { env } from "../env.mjs";
import type { Booking } from "../types/booking";
import { BOOKING_STATUS } from "../types/booking";
/**
* Fetches bookings for a user by date range.
*/
const fetchBookings = async ({
apiKey,
userId,
from,
to,
}: {
apiKey: string;
userId: number;
from: string;
to: string;
}): Promise<Booking[] | { error: string }> => {
const params = {
apiKey,
userId: userId.toString(),
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/bookings?${urlParams.toString()}`;
const response = await fetch(url);
if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
const bookings: Booking[] = data.bookings
.filter((booking: Booking) => {
const afterFrom = new Date(booking.startTime).getTime() > new Date(from).getTime();
const beforeTo = new Date(booking.endTime).getTime() < new Date(to).getTime();
const notCancelled = booking.status !== BOOKING_STATUS.CANCELLED;
return afterFrom && beforeTo && notCancelled;
})
.map(({ endTime, eventTypeId, id, startTime, status, title }: Booking) => ({
endTime,
eventTypeId,
id,
startTime,
status,
title,
}));
return bookings;
};
const getBookingsTool = (apiKey: string, userId: number) => {
return new DynamicStructuredTool({
description: "Get bookings for a user between two dates.",
func: async ({ from, to }) => {
return JSON.stringify(await fetchBookings({ apiKey, userId, from, to }));
},
name: "getBookings",
schema: z.object({
from: z.string().describe("ISO 8601 datetime string"),
to: z.string().describe("ISO 8601 datetime string"),
}),
});
};
export default getBookingsTool;
+51
View File
@@ -0,0 +1,51 @@
import { DynamicStructuredTool } from "langchain/tools";
import { z } from "zod";
import { env } from "../env.mjs";
import type { EventType } from "../types/eventType";
/**
* Fetches event types by user ID.
*/
export const fetchEventTypes = async ({ apiKey }: { apiKey: string }) => {
const params = {
apiKey,
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/event-types?${urlParams.toString()}`;
const response = await fetch(url);
if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
return data.event_types.map((eventType: EventType) => ({
id: eventType.id,
length: eventType.length,
title: eventType.title,
}));
};
const getEventTypesTool = (apiKey: string) => {
return new DynamicStructuredTool({
description: "Get the user's event type IDs. Usually necessary to book a meeting.",
func: async () => {
return JSON.stringify(
await fetchEventTypes({
apiKey,
})
);
},
name: "getEventTypes",
schema: z.object({}),
});
};
export default getEventTypesTool;
+85
View File
@@ -0,0 +1,85 @@
import { DynamicStructuredTool } from "langchain/tools";
import { z } from "zod";
import { env } from "../env.mjs";
/**
* Edits a booking for a user by booking ID with new times, title, description, or status.
*/
const editBooking = async ({
apiKey,
userId,
id,
startTime, // In the docs it says start, but it's startTime: https://cal.com/docs/enterprise-features/api/api-reference/bookings#edit-an-existing-booking.
endTime, // Same here: it says end but it's endTime.
title,
description,
status,
}: {
apiKey: string;
userId: number;
id: string;
startTime?: string;
endTime?: string;
title?: string;
description?: string;
status?: string;
}): Promise<string | { error: string }> => {
const params = {
apiKey,
userId: userId.toString(),
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/bookings/${id}?${urlParams.toString()}`;
const response = await fetch(url, {
body: JSON.stringify({ description, endTime, startTime, status, title }),
headers: {
"Content-Type": "application/json",
},
method: "PATCH",
});
// Let GPT handle this. This will happen when wrong booking id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
return "Booking edited";
};
const editBookingTool = (apiKey: string, userId: number) => {
return new DynamicStructuredTool({
description: "Edit a booking",
func: async ({ description, endTime, id, startTime, status, title }) => {
return JSON.stringify(
await editBooking({
apiKey,
userId,
description,
endTime,
id,
startTime,
status,
title,
})
);
},
name: "editBooking",
schema: z.object({
description: z.string().optional(),
endTime: z.string().optional(),
id: z.string(),
startTime: z.string().optional(),
status: z.string().optional(),
title: z.string().optional(),
}),
});
};
export default editBookingTool;
+25
View File
@@ -0,0 +1,25 @@
export type Availability = {
busy: {
start: string;
end: string;
title?: string;
}[];
timeZone: string;
dateRanges: {
start: string;
end: string;
}[];
workingHours: {
days: number[];
startTime: number;
endTime: number;
userId: number;
}[];
dateOverrides: {
date: string;
startTime: number;
endTime: number;
userId: number;
};
currentSeats: number;
};
+23
View File
@@ -0,0 +1,23 @@
export enum BOOKING_STATUS {
ACCEPTED = "ACCEPTED",
PENDING = "PENDING",
CANCELLED = "CANCELLED",
REJECTED = "REJECTED",
}
export type Booking = {
id: number;
userId: number;
description: string | null;
eventTypeId: number;
uid: string;
title: string;
startTime: string;
endTime: string;
attendees: { email: string; name: string; timeZone: string; locale: string }[] | null;
user: { email: string; name: string; timeZone: string; locale: string }[] | null;
payment: { id: number; success: boolean; paymentOption: string }[];
metadata: object | null;
status: BOOKING_STATUS;
responses: { email: string; name: string; location: string } | null;
};
+13
View File
@@ -0,0 +1,13 @@
export type EventType = {
id: number;
title: string;
length: number;
metadata: object;
slug: string;
hosts: {
userId: number;
isFixed: boolean;
}[];
hidden: boolean;
// ...
};
+9
View File
@@ -0,0 +1,9 @@
import type { EventType } from "./eventType";
import type { WorkingHours } from "./workingHours";
export type User = {
email: string;
timeZone: string;
eventTypes: EventType[];
workingHours: WorkingHours[];
};
+5
View File
@@ -0,0 +1,5 @@
export type WorkingHours = {
days: number[];
startTime: number;
endTime: number;
};
+73
View File
@@ -0,0 +1,73 @@
import { initializeAgentExecutorWithOptions } from "langchain/agents";
import { ChatOpenAI } from "langchain/chat_models/openai";
import { env } from "../env.mjs";
import createBookingIfAvailable from "../tools/createBookingIfAvailable";
import deleteBooking from "../tools/deleteBooking";
import getAvailability from "../tools/getAvailability";
import getBookings from "../tools/getBookings";
import updateBooking from "../tools/updateBooking";
import type { EventType } from "../types/eventType";
import type { User } from "../types/user";
import type { WorkingHours } from "../types/workingHours";
import now from "./now";
const gptModel = "gpt-4";
/**
* Core of the Cal AI booking agent: a LangChain Agent Executor.
* Uses a toolchain to book meetings, list available slots, etc.
* Uses OpenAI functions to better enforce JSON-parsable output from the LLM.
*/
const agent = async (input: string, user: User, apiKey: string, userId: number) => {
const tools = [
createBookingIfAvailable(apiKey, userId),
getAvailability(apiKey, userId),
getBookings(apiKey, userId),
updateBooking(apiKey, userId),
deleteBooking(apiKey),
];
const model = new ChatOpenAI({
modelName: gptModel,
openAIApiKey: env.OPENAI_API_KEY,
temperature: 0,
});
/**
* Initialize the agent executor with arguments.
*/
const executor = await initializeAgentExecutorWithOptions(tools, model, {
agentArgs: {
prefix: `You are Cal AI - a bleeding edge scheduling assistant that interfaces via email.
Make sure your final answers are definitive, complete and well formatted.
Sometimes, tools return errors. In this case, try to handle the error intelligently or ask the user for more information.
Tools will always handle times in UTC, but times sent to the user should be formatted per that user's timezone.
The current time in the user's timezone is: ${now(user.timeZone)}
The user's time zone is: ${user.timeZone}
The user's event types are: ${user.eventTypes
.map((e: EventType) => `ID: ${e.id}, Title: ${e.title}, Length: ${e.length}`)
.join("\n")}
The user's working hours are: ${user.workingHours
.map(
(w: WorkingHours) =>
`Days: ${w.days.join(", ")}, Start Time (minutes in UTC): ${
w.startTime
}, End Time (minutes in UTC): ${w.endTime}`
)
.join("\n")}
`,
},
agentType: "openai-functions",
returnIntermediateSteps: env.NODE_ENV === "development",
verbose: env.NODE_ENV === "development",
});
const result = await executor.call({ input });
const { output } = result;
return output;
};
export default agent;
+7
View File
@@ -0,0 +1,7 @@
import type { NextRequest } from "next/server";
const getHostFromHeaders = (headers: NextRequest["headers"]): string => {
return `https://${headers.get("host")}`;
};
export default getHostFromHeaders;
+5
View File
@@ -0,0 +1,5 @@
export default function now(timeZone: string) {
return new Date().toLocaleString("en-US", {
timeZone,
});
}
+40
View File
@@ -0,0 +1,40 @@
import mail from "@sendgrid/mail";
const sendgridAPIKey = process.env.SENDGRID_API_KEY as string;
/**
* Simply send an email by address, subject, and body.
*/
const send = async ({
subject,
to,
from,
text,
html,
}: {
subject: string;
to: string;
from: string;
text: string;
html?: string;
}): Promise<boolean> => {
mail.setApiKey(sendgridAPIKey);
const msg = {
to,
from: {
email: from,
name: "Cal AI",
},
text,
html,
subject,
};
const res = await mail.send(msg);
const success = !!res;
return success;
};
export default send;
+13
View File
@@ -0,0 +1,13 @@
import type { NextRequest } from "next/server";
import { env } from "../env.mjs";
/**
* Verifies that the request contains the correct parse key.
* env.PARSE_KEY must be configured as a query param in the sendgrid inbound parse settings.
*/
export const verifyParseKey = (url: NextRequest["url"]) => {
const verified = new URL(url).searchParams.get("parseKey") === env.PARSE_KEY;
return verified;
};
+18
View File
@@ -0,0 +1,18 @@
{
"extends": "@calcom/tsconfig/nextjs.json",
"compilerOptions": {
"strict": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"~/*": ["*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+3 -3
View File
@@ -1,7 +1,7 @@
import { PrismaClient } from "@prisma/client";
import type { NextMiddleware } from "next-api-middleware";
import { CONSOLE_URL } from "@calcom/lib/constants";
import { customPrisma } from "@calcom/prisma";
const LOCAL_CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL || CONSOLE_URL;
@@ -12,7 +12,7 @@ export const customPrismaClient: NextMiddleware = async (req, res, next) => {
} = req;
// If no custom api Id is provided, attach to request the regular cal.com prisma client.
if (!key) {
req.prisma = new PrismaClient();
req.prisma = customPrisma();
await next();
return;
}
@@ -26,7 +26,7 @@ export const customPrismaClient: NextMiddleware = async (req, res, next) => {
res.status(400).json({ error: "no databaseUrl set up at your instance yet" });
return;
}
req.prisma = new PrismaClient({ datasources: { db: { url: databaseUrl } } });
req.prisma = customPrisma({ datasources: { db: { url: databaseUrl } } });
/* @note:
In order to skip verifyApiKey for customPrisma requests,
we pass isAdmin true, and userId 0, if we detect them later,
+1
View File
@@ -58,6 +58,7 @@ export const schemaBookingReadPublic = Booking.extend({
})
)
.optional(),
responses: z.record(z.any()),
}).pick({
id: true,
userId: true,
+10 -1
View File
@@ -33,12 +33,14 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
position: true,
eventName: true,
timeZone: true,
schedulingType: true,
// START Limit future bookings
periodType: true,
periodStartDate: true,
schedulingType: true,
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
// END Limit future bookings
requiresConfirmation: true,
disableGuests: true,
hideCalendarNotes: true,
@@ -51,6 +53,8 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
slotInterval: true,
successRedirectUrl: true,
locations: true,
bookingLimits: true,
durationLimits: true,
})
.merge(z.object({ hosts: z.array(hostSchema).optional().default([]) }))
.partial()
@@ -66,6 +70,7 @@ const schemaEventTypeCreateParams = z
recurringEvent: recurringEventInputSchema.optional(),
seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(),
seatsShowAvailabilityCount: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(),
scheduleId: z.number().optional(),
})
@@ -85,6 +90,7 @@ const schemaEventTypeEditParams = z
length: z.number().int().optional(),
seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(),
seatsShowAvailabilityCount: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(),
scheduleId: z.number().optional(),
})
@@ -125,7 +131,10 @@ export const schemaEventTypeReadPublic = EventType.pick({
metadata: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
bookingFields: true,
bookingLimits: true,
durationLimits: true,
}).merge(
z.object({
locations: z
+12 -10
View File
@@ -13,16 +13,18 @@ const schemaMembershipRequiredParams = z.object({
teamId: z.number(),
});
export const membershipCreateBodySchema = Membership.partial({
accepted: true,
role: true,
disableImpersonation: true,
}).transform((v) => ({
accepted: false,
role: MembershipRole.MEMBER,
disableImpersonation: false,
...v,
}));
export const membershipCreateBodySchema = Membership.omit({ id: true })
.partial({
accepted: true,
role: true,
disableImpersonation: true,
})
.transform((v) => ({
accepted: false,
role: MembershipRole.MEMBER,
disableImpersonation: false,
...v,
}));
export const membershipEditBodySchema = Membership.omit({
/** To avoid complication, let's avoid updating these, instead you can delete and create a new invite */
@@ -0,0 +1,20 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryUserEmail = baseApiParams.extend({
email: z.string().email(),
});
export const schemaQuerySingleOrMultipleUserEmails = z.object({
email: z.union([z.string().email(), z.array(z.string().email())]),
});
export const withValidQueryUserEmail = withValidation({
schema: schemaQueryUserEmail,
type: "Zod",
mode: "query",
});
+1 -1
View File
@@ -1,7 +1,7 @@
import type { Session } from "next-auth";
import type { NextApiRequest as BaseNextApiRequest } from "next/types";
import type { PrismaClient } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
export type * from "next/types";
+1 -1
View File
@@ -40,6 +40,6 @@
"typescript": "^4.9.4",
"tzdata": "^1.0.30",
"uuid": "^8.3.2",
"zod": "^3.20.2"
"zod": "^3.22.2"
}
}
+3
View File
@@ -94,6 +94,9 @@ import { defaultResponder } from "@calcom/lib/server";
* seatsShowAttendees:
* type: boolean
* description: 'Share Attendee information in seats'
* seatsShowAvailabilityCount:
* type: boolean
* description: 'Show the number of available seats'
* smsReminderNumber:
* type: number
* description: 'SMS reminder number'
+12 -2
View File
@@ -1,4 +1,4 @@
import type { Prisma } from "@prisma/client";
import { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import type { z } from "zod";
@@ -146,6 +146,9 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
* seatsShowAttendees:
* type: boolean
* description: 'Share Attendee information in seats'
* seatsShowAvailabilityCount:
* type: boolean
* description: 'Show the number of available seats'
* locations:
* type: array
* description: A list of all available locations for the event type
@@ -202,10 +205,17 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
export async function patchHandler(req: NextApiRequest) {
const { prisma, query, body } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const { hosts = [], ...parsedBody } = schemaEventTypeEditBodyParams.parse(body);
const {
hosts = [],
bookingLimits,
durationLimits,
...parsedBody
} = schemaEventTypeEditBodyParams.parse(body);
const data: Prisma.EventTypeUpdateArgs["data"] = {
...parsedBody,
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
};
if (hosts) {
+1 -1
View File
@@ -2,7 +2,7 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import type { PrismaClient } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
+9 -2
View File
@@ -1,4 +1,4 @@
import type { Prisma } from "@prisma/client";
import { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
@@ -259,12 +259,19 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
async function postHandler(req: NextApiRequest) {
const { userId, isAdmin, prisma, body } = req;
const { hosts = [], ...parsedBody } = schemaEventTypeCreateBodyParams.parse(body || {});
const {
hosts = [],
bookingLimits,
durationLimits,
...parsedBody
} = schemaEventTypeCreateBodyParams.parse(body || {});
let data: Prisma.EventTypeCreateArgs["data"] = {
...parsedBody,
userId,
users: { connect: { id: userId } },
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
};
await checkPermissions(req);
+20
View File
@@ -4,6 +4,7 @@ import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import { withMiddleware } from "~/lib/helpers/withMiddleware";
import { schemaQuerySingleOrMultipleUserEmails } from "~/lib/validations/shared/queryUserEmail";
import { schemaUsersReadPublic } from "~/lib/validations/user";
/**
@@ -19,6 +20,17 @@ import { schemaUsersReadPublic } from "~/lib/validations/user";
* schema:
* type: string
* description: Your API key
* - in: query
* name: email
* required: false
* schema:
* type: array
* items:
* type: string
* format: email
* style: form
* explode: true
* description: The email address or an array of email addresses to filter by
* tags:
* - users
* responses:
@@ -39,6 +51,14 @@ export async function getHandler(req: NextApiRequest) {
const where: Prisma.UserWhereInput = {};
// If user is not ADMIN, return only his data.
if (!isAdmin) where.id = userId;
if (req.query.email) {
const validationResult = schemaQuerySingleOrMultipleUserEmails.parse(req.query);
where.email = {
in: Array.isArray(validationResult.email) ? validationResult.email : [validationResult.email],
};
}
const [total, data] = await prisma.$transaction([
prisma.user.count({ where }),
prisma.user.findMany({ where, take, skip }),
+21 -1
View File
@@ -1,9 +1,29 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
const ns = ["common"];
const supportedLngs = ["en", "fr"];
const resources = ns.reduce((acc, n) => {
supportedLngs.forEach((lng) => {
if (!acc[lng]) acc[lng] = {};
acc[lng] = {
...acc[lng],
[n]: require(`../../web/public/static/locales/${lng}/${n}.json`),
};
});
return acc;
}, {});
i18n.use(initReactI18next).init({
resources: [],
debug: true,
fallbackLng: "en",
defaultNS: "common",
ns,
interpolation: {
escapeValue: false,
},
react: { useSuspense: true },
resources,
});
export default i18n;
+8
View File
@@ -21,6 +21,14 @@ export const parameters = {
push() {},
Provider: AppRouterContext.Provider,
},
globals: {
locale: "en",
locales: {
en: "English",
fr: "Français",
},
},
i18n,
};
addDecorator((storyFn) => (
+2 -1
View File
@@ -18,6 +18,7 @@ export function VariantsTable({
const columns = React.Children.toArray(children) as ReactElement<RowProps>[];
return (
<div
id="light-variant"
className={classNames(
isDark &&
"relative py-8 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-screen before:bg-[#1C1C1C]"
@@ -43,7 +44,7 @@ export function VariantsTable({
</table>
</div>
{!isDark && (
<div data-mode="dark" className="dark">
<div id="dark-variant" data-mode="dark" className="dark">
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
{children}
</VariantsTable>
+1 -1
View File
@@ -13,7 +13,7 @@ import { Meta } from "@storybook/addon-docs";
<a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">
Figma
</a>{" "}
library is avalible for anyone to view and use. If you have any questions or concerns, please reach out to
library is available for anyone to view and use. If you have any questions or concerns, please reach out to
the design team.
</p>
</div>
+8 -8
View File
@@ -248,10 +248,10 @@
--cal-bg-inverted: #f3f4f6;
/* background -> components*/
--cal-bg-info: #dee9fc;
--cal-bg-success: #e2fbe8;
--cal-bg-attention: #fceed8;
--cal-bg-error: #f9e3e2;
--cal-bg-info: #263fa9;
--cal-bg-success: #306339;
--cal-bg-attention: #8e3b1f;
--cal-bg-error: #8c2822;
--cal-bg-dark-error: #752522;
/* Borders */
@@ -269,10 +269,10 @@
--cal-text-inverted: #101010;
/* Content/Text -> components */
--cal-text-info: #253985;
--cal-text-success: #285231;
--cal-text-attention: #73321b;
--cal-text-error: #752522;
--cal-text-info: #dee9fc;
--cal-text-success: #e2fbe8;
--cal-text-attention: #fceed8;
--cal-text-error: #f9e3e2;
/* Brand shenanigans
-> These will be computed for the users theme at runtime.
+1 -2
View File
@@ -1,2 +1 @@
public/embed
*.test.ts
public/embed
+8 -1
View File
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
import { z } from "zod";
import type { CredentialOwner } from "@calcom/app-store/types";
import classNames from "@calcom/lib/classNames";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
@@ -80,7 +81,13 @@ export default function AppListCard(props: AppListCardProps) {
return (
<div className={`${highlight ? "dark:bg-muted bg-yellow-100" : ""}`}>
<div className="flex items-center gap-x-3 px-5 py-4">
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
{logo ? (
<img
className={classNames(logo.includes("-dark") && "dark:invert", "h-10 w-10")}
src={logo}
alt={`${title} logo`}
/>
) : null}
<div className="flex grow flex-col gap-y-1 truncate">
<div className="flex items-center gap-x-2">
<h3 className="text-emphasis truncate text-sm font-semibold">{title}</h3>
@@ -116,6 +116,7 @@ function ConnectedCalendarsList(props: Props) {
type={item.integration.type}
isChecked={cal.isSelected}
destination={cal.externalId === props.destinationCalendarId}
credentialId={cal.credentialId}
/>
))}
</ul>
+29
View File
@@ -0,0 +1,29 @@
import React from "react";
import { useFormContext } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label, TextField } from "@calcom/ui";
export default function TwoFactor({ center = true }) {
const { t } = useLocale();
const methods = useFormContext();
return (
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
<Label className="mt-4">{t("backup_code")}</Label>
<p className="text-subtle mb-4 text-sm">{t("backup_code_instructions")}</p>
<TextField
id="backup-code"
label=""
defaultValue=""
placeholder="XXXXX-XXXXX"
minLength={10} // without dash
maxLength={11} // with dash
required
{...methods.register("backupCode")}
/>
</div>
);
}
+2 -2
View File
@@ -5,7 +5,7 @@ import { useFormContext } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label, Input } from "@calcom/ui";
export default function TwoFactor({ center = true }) {
export default function TwoFactor({ center = true, autoFocus = true }) {
const [value, onChange] = useState("");
const { t } = useLocale();
const methods = useFormContext();
@@ -40,7 +40,7 @@ export default function TwoFactor({ center = true }) {
name={`2fa${index + 1}`}
inputMode="decimal"
{...digit}
autoFocus={index === 0}
autoFocus={autoFocus && index === 0}
autoComplete="one-time-code"
/>
))}
@@ -336,7 +336,7 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent>
</Dialog>
<tr className="hover:bg-muted group flex flex-col sm:flex-row">
<tr data-testid="booking-item" className="hover:bg-muted group flex flex-col sm:flex-row">
<td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}>
@@ -382,24 +382,22 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
}}
/>
{selectedLocation && LocationOptions}
<DialogFooter>
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
<Button
onClick={() => {
setShowLocationModal(false);
setSelectedLocation?.(undefined);
setEditingLocationType?.("");
locationFormMethods.unregister(["locationType", "locationLink"]);
}}
type="button"
color="secondary">
{t("cancel")}
</Button>
<DialogFooter className="mt-4">
<Button
onClick={() => {
setShowLocationModal(false);
setSelectedLocation?.(undefined);
setEditingLocationType?.("");
locationFormMethods.unregister(["locationType", "locationLink"]);
}}
type="button"
color="secondary">
{t("cancel")}
</Button>
<Button data-testid="update-location" type="submit">
{t("update")}
</Button>
</div>
<Button data-testid="update-location" type="submit">
{t("update")}
</Button>
</DialogFooter>
</Form>
</div>
@@ -41,7 +41,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent>
<DialogContent enableOverflow>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<Clock className="m-auto h-6 w-6" />
@@ -377,6 +377,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
defaultChecked={!!eventType.seatsShowAttendees}
/>
</div>
<div className="mt-2">
<CheckboxField
description={t("show_available_seats_count")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)}
defaultChecked={!!eventType.seatsShowAvailabilityCount}
/>
</div>
</div>
)}
/>
@@ -13,7 +13,6 @@ import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, MeetLocationType, LocationType } 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 cx from "@calcom/lib/classNames";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
@@ -302,13 +301,7 @@ export const EventSetupTab = (
<div className="flex items-center">
<img
src={eventLocationType.iconUrl}
className={cx(
"h-4 w-4",
// invert all the icons except app icons
eventLocationType.iconUrl &&
!eventLocationType.iconUrl.startsWith("/app-store") &&
"dark:invert"
)}
className="h-4 w-4 dark:invert-[.65]"
alt={`${eventLocationType.label} logo`}
/>
<span className="ms-1 line-clamp-1 text-sm">{`${eventLabel} ${
@@ -9,7 +9,6 @@ import type { Options } from "react-select";
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { Label, Select } from "@calcom/ui";
@@ -18,13 +17,14 @@ interface IUserToValue {
id: number | null;
name: string | null;
username: string | null;
avatar: string;
email: string;
}
const mapUserToValue = ({ id, name, username, email }: IUserToValue, pendingString: string) => ({
const mapUserToValue = ({ id, name, username, avatar, email }: IUserToValue, pendingString: string) => ({
value: `${id || ""}`,
label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`,
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
avatar,
email,
});
@@ -66,6 +66,7 @@ type Props = {
formMethods: UseFormReturn<FormValues>;
isUpdateMutationLoading?: boolean;
availability?: AvailabilityOption;
isUserOrganizationAdmin: boolean;
};
function getNavigation(props: {
@@ -133,6 +134,7 @@ function EventTypeSingleLayout({
isUpdateMutationLoading,
formMethods,
availability,
isUserOrganizationAdmin,
}: Props) {
const utils = trpc.useContext();
const { t } = useLocale();
@@ -142,7 +144,8 @@ function EventTypeSingleLayout({
const hasPermsToDelete =
currentUserMembership?.role !== "MEMBER" ||
!currentUserMembership ||
eventType.schedulingType === SchedulingType.MANAGED;
eventType.schedulingType === SchedulingType.MANAGED ||
isUserOrganizationAdmin;
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => {
@@ -53,6 +53,7 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
<ul className="p-4">
{calendars?.map((calendar, i) => (
<CalendarSwitch
credentialId={calendar.credentialId}
key={calendar.externalId}
externalId={calendar.externalId}
title={calendar.name || "Nameless Calendar"}
@@ -22,7 +22,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const scheduleId = defaultScheduleId === null ? undefined : defaultScheduleId;
const queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId },
{ scheduleId: defaultScheduleId ?? undefined },
{
enabled: !!scheduleId,
}
@@ -3,12 +3,13 @@ import type { FormEvent } from "react";
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar";
import { useLocale } from "@calcom/lib/hooks/useLocale";
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 { Avatar, Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
type FormData = {
@@ -99,11 +100,11 @@ const UserProfile = () => {
<form onSubmit={onSubmit}>
<div className="flex flex-row items-center justify-start rtl:justify-end">
{user && (
<Avatar
<OrganizationAvatar
alt={user.username || "user avatar"}
gravatarFallbackMd5={user.emailMd5}
size="lg"
imageSrc={imageSrc}
organizationSlug={user.organization?.slug}
/>
)}
<input
@@ -5,6 +5,7 @@ import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, DialogFooter, Form, PasswordField } from "@calcom/ui";
import BackupCode from "@components/auth/BackupCode";
import TwoFactor from "@components/auth/TwoFactor";
import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
@@ -20,6 +21,7 @@ interface DisableTwoFactorAuthModalProps {
}
interface DisableTwoFactorValues {
backupCode: string;
totpCode: string;
password: string;
}
@@ -33,11 +35,19 @@ const DisableTwoFactorAuthModal = ({
}: DisableTwoFactorAuthModalProps) => {
const [isDisabling, setIsDisabling] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false);
const { t } = useLocale();
const form = useForm<DisableTwoFactorValues>();
async function handleDisable({ totpCode, password }: DisableTwoFactorValues) {
const resetForm = (clearPassword = true) => {
if (clearPassword) form.setValue("password", "");
form.setValue("backupCode", "");
form.setValue("totpCode", "");
setErrorMessage(null);
};
async function handleDisable({ password, totpCode, backupCode }: DisableTwoFactorValues) {
if (isDisabling) {
return;
}
@@ -45,8 +55,10 @@ const DisableTwoFactorAuthModal = ({
setErrorMessage(null);
try {
const response = await TwoFactorAuthAPI.disable(password, totpCode);
const response = await TwoFactorAuthAPI.disable(password, totpCode, backupCode);
if (response.status === 200) {
setTwoFactorLostAccess(false);
resetForm();
onDisable();
return;
}
@@ -54,12 +66,14 @@ const DisableTwoFactorAuthModal = ({
const body = await response.json();
if (body.error === ErrorCode.IncorrectPassword) {
setErrorMessage(t("incorrect_password"));
}
if (body.error === ErrorCode.SecondFactorRequired) {
} else if (body.error === ErrorCode.SecondFactorRequired) {
setErrorMessage(t("2fa_required"));
}
if (body.error === ErrorCode.IncorrectTwoFactorCode) {
} else if (body.error === ErrorCode.IncorrectTwoFactorCode) {
setErrorMessage(t("incorrect_2fa"));
} else if (body.error === ErrorCode.IncorrectBackupCode) {
setErrorMessage(t("incorrect_backup_code"));
} else if (body.error === ErrorCode.MissingBackupCodes) {
setErrorMessage(t("missing_backup_codes"));
} else {
setErrorMessage(t("something_went_wrong"));
}
@@ -78,6 +92,7 @@ const DisableTwoFactorAuthModal = ({
<div className="mb-8">
{!disablePassword && (
<PasswordField
required
labelProps={{
className: "block text-sm font-medium text-default",
}}
@@ -85,12 +100,25 @@ const DisableTwoFactorAuthModal = ({
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
/>
)}
<TwoFactor center={false} />
{twoFactorLostAccess ? (
<BackupCode center={false} />
) : (
<TwoFactor center={false} autoFocus={false} />
)}
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
</div>
<DialogFooter showDivider className="relative mt-5">
<Button
color="minimal"
className="mr-auto"
onClick={() => {
setTwoFactorLostAccess(!twoFactorLostAccess);
resetForm(false);
}}>
{twoFactorLostAccess ? t("go_back") : t("lost_access")}
</Button>
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useCallbackRef } from "@calcom/lib/hooks/useCallbackRef";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, DialogFooter, Form, TextField } from "@calcom/ui";
import { Button, Dialog, DialogContent, DialogFooter, Form, PasswordField, showToast } from "@calcom/ui";
import TwoFactor from "@components/auth/TwoFactor";
@@ -28,6 +28,7 @@ interface EnableTwoFactorModalProps {
enum SetupStep {
ConfirmPassword,
DisplayBackupCodes,
DisplayQrCode,
EnterTotpCode,
}
@@ -54,16 +55,25 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
const setupDescriptions = {
[SetupStep.ConfirmPassword]: t("2fa_confirm_current_password"),
[SetupStep.DisplayBackupCodes]: t("backup_code_instructions"),
[SetupStep.DisplayQrCode]: t("2fa_scan_image_or_use_code"),
[SetupStep.EnterTotpCode]: t("2fa_enter_six_digit_code"),
};
const [step, setStep] = useState(SetupStep.ConfirmPassword);
const [password, setPassword] = useState("");
const [backupCodes, setBackupCodes] = useState([]);
const [backupCodesUrl, setBackupCodesUrl] = useState("");
const [dataUri, setDataUri] = useState("");
const [secret, setSecret] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const resetState = () => {
setPassword("");
setErrorMessage(null);
setStep(SetupStep.ConfirmPassword);
};
async function handleSetup(e: React.FormEvent) {
e.preventDefault();
@@ -79,6 +89,15 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
const body = await response.json();
if (response.status === 200) {
setBackupCodes(body.backupCodes);
// create backup codes download url
const textBlob = new Blob([body.backupCodes.map(formatBackupCode).join("\n")], {
type: "text/plain",
});
if (backupCodesUrl) URL.revokeObjectURL(backupCodesUrl);
setBackupCodesUrl(URL.createObjectURL(textBlob));
setDataUri(body.dataUri);
setSecret(body.secret);
setStep(SetupStep.DisplayQrCode);
@@ -113,7 +132,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
const body = await response.json();
if (response.status === 200) {
onEnable();
setStep(SetupStep.DisplayBackupCodes);
return;
}
@@ -141,15 +160,19 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
}
}, [form, handleEnableRef, totpCode]);
const formatBackupCode = (code: string) => `${code.slice(0, 5)}-${code.slice(5, 10)}`;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent title={t("enable_2fa")} description={setupDescriptions[step]} type="creation">
<DialogContent
title={step === SetupStep.DisplayBackupCodes ? t("backup_codes") : t("enable_2fa")}
description={setupDescriptions[step]}
type="creation">
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<form onSubmit={handleSetup}>
<div className="mb-4">
<TextField
<PasswordField
label={t("password")}
type="password"
name="password"
id="password"
required
@@ -173,6 +196,15 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
</p>
</>
</WithStep>
<WithStep step={SetupStep.DisplayBackupCodes} current={step}>
<>
<div className="mt-5 grid grid-cols-2 gap-1 text-center font-mono md:pl-10 md:pr-10">
{backupCodes.map((code) => (
<div key={code}>{formatBackupCode(code)}</div>
))}
</div>
</>
</WithStep>
<Form handleSubmit={handleEnable} form={form}>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<div className="-mt-4 pb-2">
@@ -186,9 +218,16 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
</div>
</WithStep>
<DialogFooter className="mt-8" showDivider>
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
{step !== SetupStep.DisplayBackupCodes ? (
<Button
color="secondary"
onClick={() => {
onCancel();
resetState();
}}>
{t("cancel")}
</Button>
) : null}
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<Button
type="submit"
@@ -218,6 +257,35 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
{t("enable")}
</Button>
</WithStep>
<WithStep step={SetupStep.DisplayBackupCodes} current={step}>
<>
<Button
color="secondary"
data-testid="backup-codes-close"
onClick={(e) => {
e.preventDefault();
resetState();
onEnable();
}}>
{t("close")}
</Button>
<Button
color="secondary"
data-testid="backup-codes-copy"
onClick={(e) => {
e.preventDefault();
navigator.clipboard.writeText(backupCodes.map(formatBackupCode).join("\n"));
showToast(t("backup_codes_copied"), "success");
}}>
{t("copy")}
</Button>
<a download="cal-backup-codes.txt" href={backupCodesUrl}>
<Button color="primary" data-testid="backup-codes-download">
{t("download")}
</Button>
</a>
</>
</WithStep>
</DialogFooter>
</Form>
</DialogContent>
@@ -19,10 +19,10 @@ const TwoFactorAuthAPI = {
});
},
async disable(password: string, code: string) {
async disable(password: string, code: string, backupCode: string) {
return fetch("/api/auth/two-factor/totp/disable", {
method: "POST",
body: JSON.stringify({ password, code }),
body: JSON.stringify({ password, code, backupCode }),
headers: {
"Content-Type": "application/json",
},
+9 -27
View File
@@ -57,12 +57,10 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
}),
});
const formMethods = useForm<{
username: string;
email_address: string;
full_name: string;
password: string;
}>({
type formSchemaType = z.infer<typeof formSchema>;
const formMethods = useForm<formSchemaType>({
mode: "onChange",
resolver: zodResolver(formSchema),
});
@@ -70,7 +68,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
props.onError();
};
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
const onSubmit = formMethods.handleSubmit(async (data) => {
props.onSubmit();
const response = await fetch("/api/auth/setup", {
method: "POST",
@@ -130,11 +128,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
className={classNames("my-0", longWebsiteUrl && "rounded-t-none")}
onBlur={onBlur}
name="username"
onChange={async (e) => {
onChange(e.target.value);
formMethods.setValue("username", e.target.value);
await formMethods.trigger("username");
}}
onChange={(e) => onChange(e.target.value)}
/>
</>
)}
@@ -148,11 +142,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<TextField
value={value || ""}
onBlur={onBlur}
onChange={async (e) => {
onChange(e.target.value);
formMethods.setValue("full_name", e.target.value);
await formMethods.trigger("full_name");
}}
onChange={(e) => onChange(e.target.value)}
color={formMethods.formState.errors.full_name ? "warn" : ""}
type="text"
name="full_name"
@@ -172,11 +162,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<EmailField
value={value || ""}
onBlur={onBlur}
onChange={async (e) => {
onChange(e.target.value);
formMethods.setValue("email_address", e.target.value);
await formMethods.trigger("email_address");
}}
onChange={(e) => onChange(e.target.value)}
className="my-0"
name="email_address"
/>
@@ -191,11 +177,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<PasswordField
value={value || ""}
onBlur={onBlur}
onChange={async (e) => {
onChange(e.target.value);
formMethods.setValue("password", e.target.value);
await formMethods.trigger("password");
}}
onChange={(e) => onChange(e.target.value)}
hintErrors={["caplow", "admin_min", "num"]}
name="password"
className="my-0"
+1 -1
View File
@@ -9,7 +9,7 @@ import { Avatar } from "@calcom/ui";
type TeamType = Omit<NonNullable<TeamWithMembers>, "inviteToken">;
type MembersType = TeamType["members"];
type MemberType = MembersType[number] & { safeBio: string | null };
type MemberType = Pick<MembersType[number], "id" | "name" | "bio" | "username"> & { safeBio: string | null };
type TeamTypeWithSafeHtml = Omit<TeamType, "members" | "inviteToken"> & { members: MemberType[] };
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
+8 -10
View File
@@ -4,7 +4,6 @@ import { components } from "react-select";
import type { EventLocationType } from "@calcom/app-store/locations";
import type { CredentialDataWithTeamName } from "@calcom/app-store/utils";
import { classNames } from "@calcom/lib";
import cx from "@calcom/lib/classNames";
import { Select } from "@calcom/ui";
export type LocationOption = {
@@ -23,14 +22,7 @@ export type GroupOptionType = GroupBase<LocationOption>;
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
return (
<div className="flex items-center gap-3">
{icon && (
<img
src={icon}
alt="cover"
// invert all the icons except app icons
className={cx("h-3.5 w-3.5", icon && !icon.startsWith("/app-store") && "dark:invert")}
/>
)}
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5 dark:invert-[.65]" />}
<span className={classNames("text-sm font-medium")}>{label}</span>
</div>
);
@@ -57,7 +49,13 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
}}
formatOptionLabel={(e) => (
<div className="flex items-center gap-3">
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
{e.icon && (
<img
src={e.icon}
alt="app-icon"
className={classNames(e.icon.includes("-dark") && "dark:invert", "h-5 w-5")}
/>
)}
<span>{e.label}</span>
</div>
)}
+1 -14
View File
@@ -14,7 +14,6 @@ import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { FeatureProvider } from "@calcom/features/flags/context/provider";
import { useFlags } from "@calcom/features/flags/hooks";
import { trpc } from "@calcom/trpc/react";
import { MetaProvider } from "@calcom/ui";
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
@@ -222,19 +221,7 @@ function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
function useOrgBrandingValues() {
const session = useSession();
const res = trpc.viewer.organizations.getBrand.useQuery(undefined, {
// Only fetch if we have a session to avoid flooding logs with errors
enabled: session.status === "authenticated",
});
if (res.status === "loading") {
return undefined;
}
if (res.status === "error") return null;
return res.data;
return session?.data?.user.org;
}
function OrgBrandProvider({ children }: { children: React.ReactNode }) {
+1 -1
View File
@@ -32,7 +32,7 @@ function getCspPolicy(nonce: string) {
IS_PRODUCTION ? (useNonStrictPolicy ? "'unsafe-inline'" : "") : "'unsafe-inline'"
} app.cal.com;
font-src 'self';
img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
img-src 'self' ${WEBAPP_URL} https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
connect-src 'self'
`;
}
-11
View File
@@ -1,11 +0,0 @@
import crypto from "crypto";
export const defaultAvatarSrc = function ({ email, md5 }: { md5?: string; email?: string }) {
if (!email && !md5) return "";
if (email && !md5) {
md5 = crypto.createHash("md5").update(email).digest("hex");
}
return `https://www.gravatar.com/avatar/${md5}?s=160&d=mp&r=PG`;
};
+47 -33
View File
@@ -14,7 +14,8 @@ const {
if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET");
if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY");
const isOrganizationsEnabled =
process.env.ORGANIZATIONS_ENABLED === "1" || process.env.ORGANIZATIONS_ENABLED === "true";
// To be able to use the version in the app without having to import package.json
process.env.NEXT_PUBLIC_CALCOM_VERSION = version;
@@ -226,7 +227,7 @@ const nextConfig = {
async rewrites() {
const beforeFiles = [
// These rewrites are other than booking pages rewrites and so that they aren't redirected to org pages ensure that they happen in beforeFiles
...(process.env.ORGANIZATIONS_ENABLED
...(isOrganizationsEnabled
? [
{
...matcherConfigRootPath,
@@ -253,6 +254,10 @@ const nextConfig = {
source: "/org/:slug",
destination: "/team/:slug",
},
{
source: "/org/:orgSlug/avatar.png",
destination: "/api/user/avatar?orgSlug=:orgSlug",
},
{
source: "/team/:teamname/avatar.png",
destination: "/api/user/avatar?teamname=:teamname",
@@ -333,44 +338,46 @@ const nextConfig = {
},
],
},
...[
{
...matcherConfigRootPath,
headers: [
...(isOrganizationsEnabled
? [
{
key: "X-Cal-Org-path",
value: "/team/:orgSlug",
...matcherConfigRootPath,
headers: [
{
key: "X-Cal-Org-path",
value: "/team/:orgSlug",
},
],
},
],
},
{
...matcherConfigUserRoute,
headers: [
{
key: "X-Cal-Org-path",
value: "/org/:orgSlug/:user",
...matcherConfigUserRoute,
headers: [
{
key: "X-Cal-Org-path",
value: "/org/:orgSlug/:user",
},
],
},
],
},
{
...matcherConfigUserTypeRoute,
headers: [
{
key: "X-Cal-Org-path",
value: "/org/:orgSlug/:user/:type",
...matcherConfigUserTypeRoute,
headers: [
{
key: "X-Cal-Org-path",
value: "/org/:orgSlug/:user/:type",
},
],
},
],
},
{
...matcherConfigUserTypeEmbedRoute,
headers: [
{
key: "X-Cal-Org-path",
value: "/org/:orgSlug/:user/:type/embed",
...matcherConfigUserTypeEmbedRoute,
headers: [
{
key: "X-Cal-Org-path",
value: "/org/:orgSlug/:user/:type/embed",
},
],
},
],
},
],
]
: []),
];
},
async redirects() {
@@ -447,6 +454,13 @@ const nextConfig = {
},
{
source: "/support",
missing: [
{
type: "header",
key: "host",
value: orgHostPath,
},
],
destination: "/event-types?openIntercom=true",
permanent: true,
},
@@ -463,7 +477,7 @@ const nextConfig = {
// OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL
...(process.env.NODE_ENV === "development" &&
// Safer to enable the redirect only when the user is opting to test out organizations
process.env.ORGANIZATIONS_ENABLED &&
isOrganizationsEnabled &&
// Prevent infinite redirect by checking that we aren't already on localhost
process.env.NEXT_PUBLIC_WEBAPP_URL !== "http://localhost:3000"
? [
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.2.3",
"version": "3.3.2",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -129,7 +129,7 @@
"turndown": "^7.1.1",
"uuid": "^8.3.2",
"web3": "^1.7.5",
"zod": "^3.20.2"
"zod": "^3.22.2"
},
"devDependencies": {
"@babel/core": "^7.19.6",
+1
View File
@@ -75,6 +75,7 @@ export default function Custom404() {
)}`
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isSuccessPage = pathname?.startsWith("/booking");
+11 -4
View File
@@ -11,6 +11,7 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
@@ -25,7 +26,7 @@ import prisma from "@calcom/prisma";
import type { EventType, User } from "@calcom/prisma/client";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, HeadSeo, UnpublishedEntity } from "@calcom/ui";
import { HeadSeo, UnpublishedEntity } from "@calcom/ui";
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
import type { EmbedProps } from "@lib/withEmbedSsr";
@@ -53,7 +54,6 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
orgSlug: _orgSlug,
...query
} = useRouterQuery();
const nameOrUsername = user.name || user.username || "";
/*
const telemetry = useTelemetry();
@@ -97,7 +97,12 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
"max-w-3xl px-4 py-24"
)}>
<div className="mb-8 text-center">
<Avatar imageSrc={profile.image} size="xl" alt={profile.name} />
<OrganizationAvatar
imageSrc={profile.image}
size="xl"
alt={profile.name}
organizationSlug={profile.organizationSlug}
/>
<h1 className="font-cal text-emphasis mb-1 text-3xl" data-testid="name-title">
{profile.name}
{user.verified && (
@@ -149,7 +154,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
<div className="flex flex-wrap items-center">
<h2 className=" text-default pr-2 text-sm font-semibold">{type.title}</h2>
</div>
<EventTypeDescription eventType={type} isPublic={true} />
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
</Link>
</div>
</div>
@@ -219,6 +224,7 @@ export type UserPageProps = {
theme: string | null;
brandColor: string;
darkBrandColor: string;
organizationSlug: string | null;
allowSEOIndexing: boolean;
};
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[];
@@ -322,6 +328,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
organizationSlug: user.organization?.slug ?? null,
allowSEOIndexing: user.allowSEOIndexing ?? true,
};
+30 -48
View File
@@ -1,5 +1,4 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername";
@@ -11,18 +10,10 @@ import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import { validateUsernameInTeam, validateUsername } from "@calcom/lib/validateUsername";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { MembershipRole } from "@calcom/prisma/enums";
import { signupSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
const signupSchema = z.object({
username: z.string().refine((value) => !value.includes("+"), {
message: "String should not contain a plus symbol (+).",
}),
email: z.string().email(),
password: z.string().min(7),
language: z.string().optional(),
token: z.string().optional(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).end();
@@ -39,19 +30,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const username = slugify(data.username);
const userEmail = email.toLowerCase();
const validationResponse = (
incomingEmail: string,
validation: { isValid: boolean; email: string | undefined }
) => {
const { isValid, email } = validation;
if (!isValid) {
const message: string =
email !== incomingEmail ? "Username already taken" : "Email address is already registered";
return res.status(409).json({ message });
}
};
if (!username) {
res.status(422).json({ message: "Invalid username" });
return;
@@ -79,11 +57,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
if (foundToken?.teamId) {
const teamUserValidation = await validateUsernameInTeam(username, userEmail, foundToken?.teamId);
return validationResponse(userEmail, teamUserValidation);
if (!teamUserValidation.isValid) {
return res.status(409).json({ message: "Username or email is already taken" });
}
}
} else {
const userValidation = await validateUsername(username, userEmail);
return validationResponse(userEmail, userValidation);
if (!userValidation.isValid) {
return res.status(409).json({ message: "Username or email is already taken" });
}
}
const hashedPassword = await hashPassword(password);
@@ -94,7 +76,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: foundToken.teamId,
},
});
if (team) {
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
@@ -109,6 +90,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
// Identify the org id in an org context signup, either the invited team is an org
// or has a parentId, otherwise parentId will be null, making orgId null
const orgId = teamMetadata?.isOrganization ? team.id : team.parentId;
const user = await prisma.user.upsert({
where: { email: userEmail },
update: {
@@ -116,49 +101,46 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
password: hashedPassword,
emailVerified: new Date(Date.now()),
identityProvider: IdentityProvider.CAL,
organizationId: orgId,
},
create: {
username,
email: userEmail,
password: hashedPassword,
identityProvider: IdentityProvider.CAL,
organizationId: orgId,
},
});
if (teamMetadata?.isOrganization) {
await prisma.user.update({
where: {
id: user.id,
},
data: {
organizationId: team.id,
},
});
}
const membership = await prisma.membership.update({
const membership = await prisma.membership.upsert({
where: {
userId_teamId: { userId: user.id, teamId: team.id },
},
data: {
update: {
accepted: true,
},
create: {
userId: user.id,
teamId: team.id,
accepted: true,
role: MembershipRole.MEMBER,
},
});
closeComUpsertTeamUser(team, user, membership.role);
// Accept any child team invites for orgs.
// Accept any child team invites for orgs and create a membership for the org itself
if (team.parentId) {
// Join ORG
await prisma.user.update({
where: {
id: user.id,
},
// Create a membership for the organization itself
await prisma.membership.create({
data: {
organizationId: team.parentId,
userId: user.id,
teamId: team.parentId,
accepted: true,
role: MembershipRole.MEMBER,
},
});
/** We do a membership update twice so we can join the ORG invite if the user is invited to a team witin a ORG. */
// We do a membership update twice so we can join the ORG invite if the user is invited to a team witin a ORG
await prisma.membership.updateMany({
where: {
userId: user.id,
@@ -43,8 +43,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
}
}
// if user has 2fa
if (user.twoFactorEnabled) {
// if user has 2fa and using backup code
if (user.twoFactorEnabled && req.body.backupCode) {
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error("Missing encryption key; cannot proceed with backup code login.");
throw new Error(ErrorCode.InternalServerError);
}
if (!user.backupCodes) {
return res.status(400).json({ error: ErrorCode.MissingBackupCodes });
}
const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, process.env.CALENDSO_ENCRYPTION_KEY));
// check if user-supplied code matches one
const index = backupCodes.indexOf(req.body.backupCode.replaceAll("-", ""));
if (index === -1) {
return res.status(400).json({ error: ErrorCode.IncorrectBackupCode });
}
// we delete all stored backup codes at the end, no need to do this here
// if user has 2fa and NOT using backup code, try totp
} else if (user.twoFactorEnabled) {
if (!req.body.code) {
return res.status(400).json({ error: ErrorCode.SecondFactorRequired });
// throw new Error(ErrorCode.SecondFactorRequired);
@@ -82,6 +104,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: session.user.id,
},
data: {
backupCodes: null,
twoFactorEnabled: false,
twoFactorSecret: null,
},
@@ -1,3 +1,4 @@
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import qrcode from "qrcode";
@@ -56,11 +57,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// bytes without updating the sanity checks in the enable and login endpoints.
const secret = authenticator.generateSecret(20);
// generate backup codes with 10 character length
const backupCodes = Array.from(Array(10), () => crypto.randomBytes(5).toString("hex"));
await prisma.user.update({
where: {
id: session.user.id,
},
data: {
backupCodes: symmetricEncrypt(JSON.stringify(backupCodes), process.env.CALENDSO_ENCRYPTION_KEY),
twoFactorEnabled: false,
twoFactorSecret: symmetricEncrypt(secret, process.env.CALENDSO_ENCRYPTION_KEY),
},
@@ -70,5 +75,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const keyUri = authenticator.keyuri(name, "Cal", secret);
const dataUri = await qrcode.toDataURL(keyUri);
return res.json({ secret, keyUri, dataUri });
return res.json({ secret, keyUri, dataUri, backupCodes });
}
+7 -2
View File
@@ -5,10 +5,12 @@ import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/Cale
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import notEmpty from "@calcom/lib/notEmpty";
import prisma from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
const selectedCalendarSelectSchema = z.object({
integration: z.string(),
externalId: z.string(),
credentialId: z.number().optional(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -24,7 +26,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: session.user.id,
},
select: {
credentials: true,
credentials: {
select: credentialForCalendarServiceSelect,
},
timeZone: true,
id: true,
selectedCalendars: true,
@@ -37,7 +41,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { credentials, ...user } = userWithCredentials;
if (req.method === "POST") {
const { integration, externalId } = selectedCalendarSelectSchema.parse(req.body);
const { integration, externalId, credentialId } = selectedCalendarSelectSchema.parse(req.body);
await prisma.selectedCalendar.upsert({
where: {
userId_integration_externalId: {
@@ -50,6 +54,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
userId: user.id,
integration,
externalId,
credentialId,
},
// already exists
update: {},
+2 -2
View File
@@ -104,7 +104,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
const attendeesList = await Promise.all(attendeesListPromises);
const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar;
const evt: CalendarEvent = {
type: booking.title,
title: booking.title,
@@ -127,7 +127,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
attendees: attendeesList,
uid: booking.uid,
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [],
};
await sendOrganizerRequestReminderEmail(evt);
@@ -0,0 +1,16 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const deleted = await prisma.calendarCache.deleteMany({
where: {
// Delete all cache entries that expired before now
expiresAt: {
lte: new Date(Date.now()),
},
},
});
res.json({ ok: true, count: deleted.count });
}
@@ -0,0 +1,340 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { sendMonthlyDigestEmails } from "@calcom/emails/email-manager";
import { EventsInsights } from "@calcom/features/insights/server/events";
import { getTranslation } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
const querySchema = z.object({
page: z.coerce.number().min(0).optional().default(0),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
if (req.method !== "POST") {
res.status(405).json({ message: "Invalid method" });
return;
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
let { page: pageNumber } = querySchema.parse(req.query);
const firstDateOfMonth = new Date();
firstDateOfMonth.setDate(1);
while (true) {
const teams = await prisma.team.findMany({
where: {
slug: {
not: null,
},
createdAt: {
// created before or on the first day of this month
lte: firstDateOfMonth,
},
},
select: {
id: true,
createdAt: true,
members: true,
name: true,
},
skip: pageNumber * pageSize,
take: pageSize,
});
if (teams.length === 0) {
break;
}
for (const team of teams) {
const EventData: {
Created: number;
Completed: number;
Rescheduled: number;
Cancelled: number;
mostBookedEvents: {
eventTypeId?: number | null;
eventTypeName?: string | null;
count?: number | null;
}[];
membersWithMostBookings: {
userId: number | null;
user: {
id: number;
name: string | null;
email: string;
avatar: string | null;
username: string | null;
};
count: number;
}[];
admin: { email: string; name: string };
team: {
name: string;
id: number;
};
} = {
Created: 0,
Completed: 0,
Rescheduled: 0,
Cancelled: 0,
mostBookedEvents: [],
membersWithMostBookings: [],
admin: { email: "", name: "" },
team: { name: team.name, id: team.id },
};
const userIdsFromTeams = team.members.map((u) => u.userId);
// Booking Events
const whereConditional: Prisma.BookingTimeStatusWhereInput = {
OR: [
{
teamId: team.id,
},
{
userId: {
in: userIdsFromTeams,
},
teamId: null,
},
],
};
const promisesResult = await Promise.all([
EventsInsights.getCreatedEventsInTimeRange(
{
start: dayjs(firstDateOfMonth),
end: dayjs(new Date()),
},
whereConditional
),
EventsInsights.getCompletedEventsInTimeRange(
{
start: dayjs(firstDateOfMonth),
end: dayjs(new Date()),
},
whereConditional
),
EventsInsights.getRescheduledEventsInTimeRange(
{
start: dayjs(firstDateOfMonth),
end: dayjs(new Date()),
},
whereConditional
),
EventsInsights.getCancelledEventsInTimeRange(
{
start: dayjs(firstDateOfMonth),
end: dayjs(new Date()),
},
whereConditional
),
]);
EventData["Created"] = promisesResult[0];
EventData["Completed"] = promisesResult[1];
EventData["Rescheduled"] = promisesResult[2];
EventData["Cancelled"] = promisesResult[3];
// Most Booked Event Type
const bookingWhere: Prisma.BookingTimeStatusWhereInput = {
createdAt: {
gte: dayjs(firstDateOfMonth).startOf("day").toDate(),
lte: dayjs(new Date()).endOf("day").toDate(),
},
OR: [
{
teamId: team.id,
},
{
userId: {
in: userIdsFromTeams,
},
teamId: null,
},
],
};
const bookingsFromSelected = await prisma.bookingTimeStatus.groupBy({
by: ["eventTypeId"],
where: bookingWhere,
_count: {
id: true,
},
orderBy: {
_count: {
id: "desc",
},
},
take: 10,
});
const eventTypeIds = bookingsFromSelected
.filter((booking) => typeof booking.eventTypeId === "number")
.map((booking) => booking.eventTypeId);
const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {
id: {
in: eventTypeIds as number[],
},
};
const eventTypesFrom = await prisma.eventType.findMany({
select: {
id: true,
title: true,
teamId: true,
userId: true,
slug: true,
users: {
select: {
username: true,
},
},
team: {
select: {
slug: true,
},
},
},
where: eventTypeWhereConditional,
});
const eventTypeHashMap: Map<
number,
Prisma.EventTypeGetPayload<{
select: {
id: true;
title: true;
teamId: true;
userId: true;
slug: true;
users: {
select: {
username: true;
};
};
team: {
select: {
slug: true;
};
};
};
}>
> = new Map();
eventTypesFrom.forEach((eventType) => {
eventTypeHashMap.set(eventType.id, eventType);
});
EventData["mostBookedEvents"] = bookingsFromSelected.map((booking) => {
const eventTypeSelected = eventTypeHashMap.get(booking.eventTypeId ?? 0);
if (!eventTypeSelected) {
return {};
}
let eventSlug = "";
if (eventTypeSelected.userId) {
eventSlug = `${eventTypeSelected?.users[0]?.username}/${eventTypeSelected?.slug}`;
}
if (eventTypeSelected?.team && eventTypeSelected?.team?.slug) {
eventSlug = `${eventTypeSelected.team.slug}/${eventTypeSelected.slug}`;
}
return {
eventTypeId: booking.eventTypeId,
eventTypeName: eventSlug,
count: booking._count.id,
};
});
// Most booked members
const bookingsFromTeam = await prisma.bookingTimeStatus.groupBy({
by: ["userId"],
where: bookingWhere,
_count: {
id: true,
},
orderBy: {
_count: {
id: "desc",
},
},
take: 10,
});
const userIds = bookingsFromTeam
.filter((booking) => typeof booking.userId === "number")
.map((booking) => booking.userId);
if (userIds.length === 0) {
EventData["membersWithMostBookings"] = [];
} else {
const teamUsers = await prisma.user.findMany({
where: {
id: {
in: userIds as number[],
},
},
select: { id: true, name: true, email: true, avatar: true, username: true },
});
const userHashMap = new Map();
teamUsers.forEach((user) => {
userHashMap.set(user.id, user);
});
EventData["membersWithMostBookings"] = bookingsFromTeam.map((booking) => {
return {
userId: booking.userId,
user: userHashMap.get(booking.userId),
count: booking._count.id,
};
});
}
// Send mail to all Owners and Admins
const mailReceivers = team?.members?.filter(
(member) => member.role === "OWNER" || member.role === "ADMIN"
);
const mailsToSend = mailReceivers.map(async (receiver) => {
const owner = await prisma.user.findUnique({
where: {
id: receiver?.userId,
},
});
if (owner) {
const t = await getTranslation(owner?.locale ?? "en", "common");
// Only send email if user has allowed to receive monthly digest emails
if (owner.receiveMonthlyDigestEmail) {
await sendMonthlyDigestEmails({
...EventData,
admin: { email: owner?.email ?? "", name: owner?.name ?? "" },
language: t,
});
}
}
});
await Promise.all(mailsToSend);
await delay(100); // Adjust the delay as needed to avoid rate limiting
}
pageNumber++;
}
res.json({ ok: true });
}
@@ -0,0 +1 @@
export { default } from "@calcom/features/webhooks/lib/cron";
+43 -55
View File
@@ -7,68 +7,56 @@ import { getTranslation } from "@calcom/lib/server/i18n";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (IS_PRODUCTION) return res.write("Only for development purposes"), res.end();
const t = await getTranslation("en", "common");
const language = { translate: t, locale: "en" };
const evt = {
type: "30min",
title: "30min between Pro Example and pro@example.com",
description: null,
additionalNotes: "asdasdas",
startTime: "2022-06-03T09:00:00-06:00",
endTime: "2022-06-03T09:30:00-06:00",
organizer: {
name: "Pro Example",
email: "pro@example.com",
timeZone: "Europe/London",
language,
},
attendees: [
{
email: "pro@example.com",
name: "pro@example.com",
timeZone: "America/Chihuahua",
language,
},
],
location: "Zoom video",
destinationCalendar: null,
hideCalendarNotes: false,
uid: "xxyPr4cg2xx4XoS2KeMEQy",
metadata: {},
recurringEvent: null,
appsStatus: [
{
appName: "Outlook Calendar",
type: "office365_calendar",
success: 1,
failures: 0,
errors: [],
warnings: [],
},
{
appName: "Google Meet",
type: "conferencing",
success: 0,
failures: 1,
errors: [],
warnings: ["In order to use Google Meet you must set your destination calendar to a Google Calendar"],
},
],
};
res.statusCode = 200;
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write(
renderEmail("VerifyAccountEmail", {
renderEmail("MonthlyDigestEmail", {
language: t,
user: {
name: "Pro Example",
email: "pro@example.com",
},
verificationEmailLink:
"http://localhost:3000/api/auth/verify-email?token=b91af0eee5a9a24a8d83a3d3d6a58c1606496e94ced589441649273c66100f5b",
Created: 12,
Completed: 13,
Rescheduled: 14,
Cancelled: 16,
mostBookedEvents: [
{
eventTypeId: 3,
eventTypeName: "Test1",
count: 3,
},
{
eventTypeId: 4,
eventTypeName: "Test2",
count: 5,
},
],
membersWithMostBookings: [
{
userId: 4,
user: {
id: 4,
name: "User1 name",
email: "email.com",
avatar: "none",
username: "User1",
},
count: 4,
},
{
userId: 6,
user: {
id: 6,
name: "User2 name",
email: "email2.com",
avatar: "none",
username: "User2",
},
count: 8,
},
],
admin: { email: "admin.com", name: "admin" },
team: { name: "Team1", id: 4 },
})
);
res.end();
+1 -1
View File
@@ -187,7 +187,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
res.setHeader("Content-Type", response.headers.get("content-type") as string);
res.setHeader("Cache-Control", "s-maxage=86400");
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
res.send(buffer);
} catch (error) {
res.statusCode = 404;
+6 -1
View File
@@ -34,13 +34,17 @@ const triggerWebhook = async ({
booking: {
userId: number | undefined;
eventTypeId: number | null;
eventTypeParentId: number | null | undefined;
teamId?: number | null;
};
}) => {
const eventTrigger: WebhookTriggerEvents = "RECORDING_READY";
// Send Webhook call if hooked to BOOKING.RECORDING_READY
const triggerForUser = !booking.teamId || (booking.teamId && booking.eventTypeParentId);
const subscriberOptions = {
userId: booking.userId,
userId: triggerForUser ? booking.userId : null,
eventTypeId: booking.eventTypeId,
triggerEvent: eventTrigger,
teamId: booking.teamId,
@@ -183,6 +187,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
booking: {
userId: booking?.user?.id,
eventTypeId: booking.eventTypeId,
eventTypeParentId: booking.eventType?.parentId,
teamId,
},
});
+23 -11
View File
@@ -1,17 +1,16 @@
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import prisma from "@calcom/prisma";
import { defaultAvatarSrc } from "@lib/profile";
const querySchema = z
.object({
username: z.string(),
teamname: z.string(),
orgSlug: z.string(),
/**
* Allow fetching avatar of a particular organization
* Avatars being public, we need not worry about others accessing it.
@@ -21,7 +20,7 @@ const querySchema = z
.partial();
async function getIdentityData(req: NextApiRequest) {
const { username, teamname, orgId } = querySchema.parse(req.query);
const { username, teamname, orgId, orgSlug } = querySchema.parse(req.query);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
const org = isValidOrgDomain ? currentOrgDomain : null;
@@ -61,7 +60,23 @@ async function getIdentityData(req: NextApiRequest) {
org,
name: teamname,
email: null,
avatar: team?.logo || getPlaceholderAvatar(null, teamname),
avatar: getPlaceholderAvatar(team?.logo, teamname),
};
}
if (orgSlug) {
const org = await prisma.team.findFirst({
where: getSlugOrRequestedSlug(orgSlug),
select: {
slug: true,
logo: true,
name: true,
},
});
return {
org: org?.slug,
name: org?.name,
email: null,
avatar: getPlaceholderAvatar(org?.logo, org?.name),
};
}
}
@@ -69,18 +84,15 @@ async function getIdentityData(req: NextApiRequest) {
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const identity = await getIdentityData(req);
const img = identity?.avatar;
// We cache for one day
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
// If image isn't set or links to this route itself, use default avatar
if (!img) {
if (identity?.org) {
res.setHeader("x-cal-org", identity.org);
}
res.writeHead(302, {
Location: defaultAvatarSrc({
md5: crypto
.createHash("md5")
.update(identity?.email || "guest@example.com")
.digest("hex"),
}),
Location: AVATAR_FALLBACK,
});
return res.end();
@@ -0,0 +1,93 @@
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
const appCredentialWebhookRequestBodySchema = z.object({
// UserId of the cal.com user
userId: z.number().int(),
appSlug: z.string(),
// Keys should be AES256 encrypted with the CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY
keys: z.string(),
});
/** */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Check that credential sharing is enabled
if (!APP_CREDENTIAL_SHARING_ENABLED) {
return res.status(403).json({ message: "Credential sharing is not enabled" });
}
// Check that the webhook secret matches
if (
req.headers[process.env.CALCOM_WEBHOOK_HEADER_NAME || "calcom-webhook-secret"] !==
process.env.CALCOM_WEBHOOK_SECRET
) {
return res.status(403).json({ message: "Invalid webhook secret" });
}
const reqBody = appCredentialWebhookRequestBodySchema.parse(req.body);
// Check that the user exists
const user = await prisma.user.findUnique({ where: { id: reqBody.userId } });
if (!user) {
return res.status(404).json({ message: "User not found" });
}
const app = await prisma.app.findUnique({
where: { slug: reqBody.appSlug },
select: { slug: true },
});
if (!app) {
return res.status(404).json({ message: "App not found" });
}
// Search for the app's slug and type
const appMetadata = appStoreMetadata[app.slug as keyof typeof appStoreMetadata];
if (!appMetadata) {
return res.status(404).json({ message: "App not found. Ensure that you have the correct app slug" });
}
// Decrypt the keys
const keys = JSON.parse(
symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
);
// Can't use prisma upsert as we don't know the id of the credential
const appCredential = await prisma.credential.findFirst({
where: {
userId: reqBody.userId,
appId: appMetadata.slug,
},
select: {
id: true,
},
});
if (appCredential) {
await prisma.credential.update({
where: {
id: appCredential.id,
},
data: {
key: keys,
},
});
return res.status(200).json({ message: `Credentials updated for userId: ${reqBody.userId}` });
} else {
await prisma.credential.create({
data: {
key: keys,
userId: reqBody.userId,
appId: appMetadata.slug,
type: appMetadata.type,
},
});
return res.status(200).json({ message: `Credentials created for userId: ${reqBody.userId}` });
}
}
+9 -1
View File
@@ -12,7 +12,14 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/enums";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import type { HorizontalTabItemProps } from "@calcom/ui";
import { AllApps, AppStoreCategories, HorizontalTabs, TextField, PopularAppsSlider } from "@calcom/ui";
import {
AllApps,
AppStoreCategories,
HorizontalTabs,
TextField,
PopularAppsSlider,
RecentAppsSlider,
} from "@calcom/ui";
import { Search } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
@@ -81,6 +88,7 @@ export default function Apps({
<>
<AppStoreCategories categories={categories} />
<PopularAppsSlider items={appStore} />
<RecentAppsSlider items={appStore} />
</>
)}
<AllApps
+36 -12
View File
@@ -21,7 +21,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
import { ArrowLeft } from "@calcom/ui/components/icon";
import { ArrowLeft, Lock } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { WithNonceProps } from "@lib/withNonce";
@@ -29,6 +29,7 @@ import withNonce from "@lib/withNonce";
import AddToHomescreen from "@components/AddToHomescreen";
import PageWrapper from "@components/PageWrapper";
import BackupCode from "@components/auth/BackupCode";
import TwoFactor from "@components/auth/TwoFactor";
import AuthContainer from "@components/ui/AuthContainer";
@@ -39,6 +40,7 @@ interface LoginValues {
email: string;
password: string;
totpCode: string;
backupCode: string;
csrfToken: string;
}
export default function Login({
@@ -65,6 +67,7 @@ export default function Login({
const methods = useForm<LoginValues>({ resolver: zodResolver(formSchema) });
const { register, formState } = methods;
const [twoFactorRequired, setTwoFactorRequired] = useState(!!totpEmail || false);
const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const errorMessages: { [key: string]: string } = {
@@ -98,15 +101,35 @@ export default function Login({
);
const TwoFactorFooter = (
<Button
onClick={() => {
setTwoFactorRequired(false);
methods.setValue("totpCode", "");
}}
StartIcon={ArrowLeft}
color="minimal">
{t("go_back")}
</Button>
<>
<Button
onClick={() => {
if (twoFactorLostAccess) {
setTwoFactorLostAccess(false);
methods.setValue("backupCode", "");
} else {
setTwoFactorRequired(false);
methods.setValue("totpCode", "");
}
setErrorMessage(null);
}}
StartIcon={ArrowLeft}
color="minimal">
{t("go_back")}
</Button>
{!twoFactorLostAccess ? (
<Button
onClick={() => {
setTwoFactorLostAccess(true);
setErrorMessage(null);
methods.setValue("totpCode", "");
}}
StartIcon={Lock}
color="minimal">
{t("lost_access")}
</Button>
) : null}
</>
);
const ExternalTotpFooter = (
@@ -130,8 +153,9 @@ export default function Login({
if (!res) setErrorMessage(errorMessages[ErrorCode.InternalServerError]);
// we're logged in! let's do a hard refresh to the desired url
else if (!res.error) router.push(callbackUrl);
// reveal two factor input if required
else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true);
else if (res.error === ErrorCode.IncorrectBackupCode) setErrorMessage(t("incorrect_backup_code"));
else if (res.error === ErrorCode.MissingBackupCodes) setErrorMessage(t("missing_backup_codes"));
// fallback if error not found
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
};
@@ -194,7 +218,7 @@ export default function Login({
</div>
</div>
{twoFactorRequired && <TwoFactor center />}
{twoFactorRequired ? !twoFactorLostAccess ? <TwoFactor center /> : <BackupCode center /> : null}
{errorMessage && <Alert severity="error" title={errorMessage} />}
<Button
+3 -8
View File
@@ -7,13 +7,8 @@ import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/li
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import stripe from "@calcom/features/ee/payments/server/stripe";
import {
hostedCal,
isSAMLLoginEnabled,
samlProductID,
samlTenantID,
samlTenantProduct,
} from "@calcom/features/ee/sso/lib/saml";
import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import prisma from "@calcom/prisma";
@@ -111,7 +106,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
error = "Email not provided";
} else {
try {
const ret = await samlTenantProduct(prisma, emailParam);
const ret = await ssoTenantProduct(prisma, emailParam);
tenant = ret.tenant;
product = ret.product;
} catch (e) {
+103 -38
View File
@@ -6,6 +6,7 @@ import dayjs from "@calcom/dayjs";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
import Schedule from "@calcom/features/schedules/components/Schedule";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { availabilityAsString } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
@@ -17,11 +18,6 @@ import {
ConfirmationDialogContent,
Dialog,
DialogTrigger,
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuSeparator,
DropdownMenuTrigger,
Form,
Label,
showToast,
@@ -32,7 +28,7 @@ import {
Tooltip,
VerticalDivider,
} from "@calcom/ui";
import { Info, MoreHorizontal, Plus, Trash } from "@calcom/ui/components/icon";
import { Info, MoreVertical, ArrowLeft, Plus, Trash } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
@@ -95,7 +91,7 @@ export default function Availability() {
const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
const fromEventType = searchParams?.get("fromEventType");
const { timeFormat } = me.data || { timeFormat: null };
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [openSidebar, setOpenSidebar] = useState(false);
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId },
{
@@ -225,33 +221,60 @@ export default function Availability() {
</ConfirmationDialogContent>
</Dialog>
<VerticalDivider className="hidden sm:inline" />
<Dropdown>
<DropdownMenuTrigger asChild>
<Button className="sm:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent style={{ minWidth: "200px" }}>
<DropdownItem
type="button"
color="destructive"
StartIcon={Trash}
onClick={() => setDeleteDialogOpen(true)}>
{t("delete")}
</DropdownItem>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
variety="danger"
title={t("delete_schedule")}
confirmBtnText={t("delete")}
loadingText={t("delete")}
onConfirm={() => {
schedule !== undefined && deleteMutation.mutate({ scheduleId: schedule.id });
}}>
{t("delete_schedule_description")}
</ConfirmationDialogContent>
</Dialog>
<DropdownMenuSeparator />
<div className="flex h-9 flex-row items-center justify-between px-4 py-2 hover:bg-gray-100">
<div
className={classNames(
openSidebar
? "fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 sm:hidden"
: ""
)}>
<div
className={classNames(
"bg-default fixed right-0 z-20 flex h-screen w-80 flex-col space-y-2 overflow-x-hidden rounded-md px-2 pb-3 transition-transform",
openSidebar ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
)}>
<div className="flex flex-row items-center pt-5">
<Button StartIcon={ArrowLeft} color="minimal" onClick={() => setOpenSidebar(false)} />
<p className="-ml-2">{t("availability_settings")}</p>
<Dialog>
<DialogTrigger asChild>
<Button
StartIcon={Trash}
variant="icon"
color="destructive"
aria-label={t("delete")}
className="ml-16 inline"
disabled={schedule?.isLastSchedule}
tooltip={schedule?.isLastSchedule ? t("requires_at_least_one_schedule") : t("delete")}
/>
</DialogTrigger>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
variety="danger"
title={t("delete_schedule")}
confirmBtnText={t("delete")}
loadingText={t("delete")}
onConfirm={() => {
scheduleId && deleteMutation.mutate({ scheduleId });
setOpenSidebar(false);
}}>
{t("delete_schedule_description")}
</ConfirmationDialogContent>
</Dialog>
</div>
<div className="flex flex-col px-2 py-2">
<Skeleton as={Label}>{t("name")}</Skeleton>
<Controller
control={form.control}
name="name"
render={({ field }) => (
<input
className="hover:border-emphasis dark:focus:border-emphasis border-default bg-default placeholder:text-muted text-emphasis focus:ring-brand-default disabled:bg-subtle disabled:hover:border-subtle mb-2 block h-9 w-full rounded-md border px-3 py-2 text-sm leading-4 focus:border-neutral-300 focus:outline-none focus:ring-2 disabled:cursor-not-allowed"
{...field}
/>
)}
/>
</div>
<div className="flex h-9 flex-row-reverse items-center justify-end gap-3 px-2">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
@@ -267,9 +290,44 @@ export default function Availability() {
}}
/>
</div>
</DropdownMenuContent>
</Dropdown>
<div className="min-w-40 col-span-3 space-y-2 px-2 py-4 lg:col-span-1">
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
<div>
<Skeleton as={Label} htmlFor="timeZone" className="mb-0 inline-block leading-none">
{t("timezone")}
</Skeleton>
<Controller
control={form.control}
name="timeZone"
render={({ field: { onChange, value } }) =>
value ? (
<TimezoneSelect
value={value}
className="focus:border-brand-default border-default mt-1 block w-72 rounded-md text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
) : (
<SelectSkeletonLoader className="mt-1 w-72" />
)
}
/>
</div>
<hr className="border-subtle my-7" />
<div className="rounded-md md:block">
<Skeleton as="h3" className="mb-0 inline-block text-sm font-medium">
{t("something_doesnt_look_right")}
</Skeleton>
<div className="mt-3 flex">
<Skeleton as={Button} href="/availability/troubleshoot" color="secondary">
{t("launch_troubleshooter")}
</Skeleton>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="border-default border-l-2" />
<Button
className="ml-4 lg:ml-0"
@@ -278,6 +336,13 @@ export default function Availability() {
loading={updateMutation.isLoading}>
{t("save")}
</Button>
<Button
className="ml-3 sm:hidden"
StartIcon={MoreVertical}
variant="icon"
color="secondary"
onClick={() => setOpenSidebar(true)}
/>
</div>
}>
<div className="mt-4 w-full md:mt-0">
@@ -313,7 +378,7 @@ export default function Availability() {
{schedule?.workingHours && <DateOverride workingHours={schedule.workingHours} />}
</div>
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="min-w-40 col-span-3 hidden space-y-2 md:block lg:col-span-1">
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
<div>
<Skeleton as={Label} htmlFor="timeZone" className="mb-0 inline-block leading-none">
@@ -335,7 +400,7 @@ export default function Availability() {
/>
</div>
<hr className="border-subtle my-6 mr-8" />
<div className="hidden rounded-md md:block">
<div className="rounded-md">
<Skeleton as="h3" className="mb-0 inline-block text-sm font-medium">
{t("something_doesnt_look_right")}
</Skeleton>
+3 -1
View File
@@ -461,7 +461,7 @@ export default function Success(props: SuccessProps) {
{locationToDisplay && !isCancelled && (
<>
<div className="mt-3 font-medium">{t("where")}</div>
<div className="col-span-2 mt-3">
<div className="col-span-2 mt-3" data-testid="where">
{locationToDisplay.startsWith("http") ? (
<a
href={locationToDisplay}
@@ -917,6 +917,7 @@ const getEventTypesFromDB = async (id: number) => {
metadata: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
periodStartDate: true,
periodEndDate: true,
},
@@ -940,6 +941,7 @@ const handleSeatsEventTypeOnBooking = async (
eventType: {
seatsPerTimeSlot?: number | null;
seatsShowAttendees: boolean | null;
seatsShowAvailabilityCount: boolean | null;
[x: string | number | symbol]: unknown;
},
bookingInfo: Partial<
+1 -1
View File
@@ -179,7 +179,7 @@ export default function Bookings() {
)}
<div className="pt-2 xl:pt-0">
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<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}>
+20 -9
View File
@@ -113,6 +113,7 @@ export type FormValues = {
periodDates: { startDate: Date; endDate: Date };
seatsPerTimeSlot: number | null;
seatsShowAttendees: boolean | null;
seatsShowAvailabilityCount: boolean | null;
seatsPerTimeSlotEnabled: boolean;
minimumBookingNotice: number;
minimumBookingNoticeInDurationType: number;
@@ -183,12 +184,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
created: true,
}))
);
showToast(
t("event_type_updated_successfully", {
eventTypeTitle: eventType.title,
}),
"success"
);
showToast(t("event_type_updated_successfully"), "success");
},
async onSettled() {
await utils.viewer.eventTypes.get.invalidate();
@@ -365,6 +361,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
afterBufferTime,
seatsPerTimeSlot,
seatsShowAttendees,
seatsShowAvailabilityCount,
bookingLimits,
durationLimits,
recurringEvent,
@@ -381,9 +378,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
bookerLayouts,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
multipleDurationEnabled,
length,
...input
} = values;
if (!Number(length)) throw new Error(t("event_setup_length_error"));
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
@@ -401,7 +401,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
if (!length && !metadata?.multipleDuration?.includes(length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
@@ -411,9 +411,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
throw new Error(t("seats_and_no_show_fee_error"));
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, ...rest } = input;
updateMutation.mutate({
...rest,
length,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
@@ -426,6 +428,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,
seatsShowAvailabilityCount,
metadata,
customInputs,
children,
@@ -447,7 +450,8 @@ const EventTypePage = (props: EventTypeSetupProps) => {
isUpdateMutationLoading={updateMutation.isLoading}
formMethods={formMethods}
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
currentUserMembership={currentUserMembership}>
currentUserMembership={currentUserMembership}
isUserOrganizationAdmin={props.isUserOrganizationAdmin}>
<Form
form={formMethods}
id="event-type-form"
@@ -459,6 +463,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
afterBufferTime,
seatsPerTimeSlot,
seatsShowAttendees,
seatsShowAvailabilityCount,
bookingLimits,
durationLimits,
recurringEvent,
@@ -470,9 +475,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
seatsPerTimeSlotEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
multipleDurationEnabled,
length,
...input
} = values;
if (!Number(length)) throw new Error(t("event_setup_length_error"));
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
@@ -490,14 +498,16 @@ const EventTypePage = (props: EventTypeSetupProps) => {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
if (!length && !metadata?.multipleDuration?.includes(length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, ...rest } = input;
updateMutation.mutate({
...rest,
length,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
@@ -510,6 +520,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,
seatsShowAvailabilityCount,
metadata,
customInputs,
});
+74 -34
View File
@@ -52,10 +52,9 @@ import {
Skeleton,
Switch,
Tooltip,
ArrowButton,
} from "@calcom/ui";
import {
ArrowDown,
ArrowUp,
Clipboard,
Code,
Copy,
@@ -109,8 +108,10 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
const orgBranding = useOrgBranding();
const tabs = eventTypeGroups.map((item) => ({
name: item.profile.name ?? "",
href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types",
avatar: item.profile.image ?? `${orgBranding?.fullDomain ?? WEBAPP_URL}/${item.profile.slug}/avatar.png`,
href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types?noTeam",
avatar: orgBranding
? `${orgBranding.fullDomain}${item.teamId ? "/team" : ""}/${item.profile.slug}/avatar.png`
: item.profile.image ?? `${WEBAPP_URL + (item.teamId && "/team")}/${item.profile.slug}/avatar.png`,
}));
const { data } = useTypedQuery(querySchema);
const events = eventTypeGroups.filter((item) => item.teamId === data.teamId);
@@ -118,13 +119,15 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
return (
<div>
<HorizontalTabs tabs={tabs} />
{events.length && (
{events.length > 0 ? (
<EventTypeList
types={events[0].eventTypes}
group={events[0]}
groupIndex={0}
readOnly={events[0].metadata.readOnly}
/>
) : (
<CreateFirstEventTypeView />
)}
</div>
);
@@ -359,6 +362,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
}
}, []);
if (!types.length) {
return group.teamId ? <EmptyEventTypeList group={group} /> : <CreateFirstEventTypeView />;
}
const firstItem = types[0];
const lastItem = types[types.length - 1];
const isManagedEventPrefix = () => {
@@ -378,19 +385,11 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<div className="hover:bg-muted flex w-full items-center justify-between">
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden px-4 py-4 sm:px-6">
{!(firstItem && firstItem.id === type.id) && (
<button
className="bg-default text-muted hover:text-emphasis border-default hover:border-emphasis invisible absolute left-[5px] -ml-4 -mt-4 mb-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
onClick={() => moveEventType(index, -1)}>
<ArrowUp className="h-5 w-5" />
</button>
<ArrowButton onClick={() => moveEventType(index, -1)} arrowDirection="up" />
)}
{!(lastItem && lastItem.id === type.id) && (
<button
className="bg-default text-muted border-default hover:text-emphasis hover:border-emphasis invisible absolute left-[5px] -ml-4 mt-8 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
onClick={() => moveEventType(index, 1)}>
<ArrowDown className="h-5 w-5" />
</button>
<ArrowButton onClick={() => moveEventType(index, 1)} arrowDirection="down" />
)}
<MemoizedItem type={type} group={group} readOnly={readOnly} />
<div className="mt-4 hidden sm:mt-0 sm:flex">
@@ -400,23 +399,27 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
className="relative right-3 top-1"
size="sm"
truncateAfter={4}
items={type.users.map(
(organizer: { name: string | null; username: string | null }) => ({
alt: organizer.name || "",
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${
organizer.username
}/avatar.png`,
title: organizer.name || "",
})
)}
items={
type?.users
? type.users.map(
(organizer: { name: string | null; username: string | null }) => ({
alt: organizer.name || "",
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${
organizer.username
}/avatar.png`,
title: organizer.name || "",
})
)
: []
}
/>
)}
{isManagedEventType && (
{isManagedEventType && type?.children && type.children?.length > 0 && (
<AvatarGroup
className="relative right-3 top-1"
size="sm"
truncateAfter={4}
items={type.children
items={type?.children
.flatMap((ch) => ch.users)
.map((user: Pick<User, "name" | "username">) => ({
alt: user.name || "",
@@ -671,6 +674,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
title={t(`delete${isManagedEventPrefix()}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
isLoading={deleteMutation.isLoading}
onConfirm={(e) => {
e.preventDefault();
deleteEventTypeHandler(deleteDialogTypeId);
@@ -711,12 +715,17 @@ const EventTypeListHeading = ({
},
});
const bookerUrl = useBookerUrl();
return (
<div className="mb-4 flex items-center space-x-2">
<Avatar
alt={profile?.name || ""}
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
imageSrc={`${orgBranding?.fullDomain ?? WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
imageSrc={
orgBranding?.fullDomain
? `${orgBranding.fullDomain}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`
: profile.image
}
size="md"
className="mt-1 inline-flex justify-center"
/>
@@ -763,6 +772,12 @@ const CreateFirstEventTypeView = () => {
Icon={LinkIcon}
headline={t("new_event_type_heading")}
description={t("new_event_type_description")}
className="mb-16"
buttonRaw={
<Button href="?dialog=new" variant="button">
{t("create")}
</Button>
}
/>
);
};
@@ -830,6 +845,25 @@ const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
);
};
const EmptyEventTypeList = ({ group }: { group: EventTypeGroup }) => {
const { t } = useLocale();
return (
<>
<EmptyScreen
headline={t("team_no_event_types")}
buttonRaw={
<Button
href={`?dialog=new&eventPage=${group.profile.slug}&teamId=${group.teamId}`}
variant="button"
className="mt-5">
{t("create")}
</Button>
}
/>
</>
);
};
const Main = ({
status,
errorMessage,
@@ -863,7 +897,7 @@ const Main = ({
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
) : (
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
<div className="flex flex-col" key={group.profile.slug}>
<div className="mt-4 flex flex-col" key={group.profile.slug}>
<EventTypeListHeading
profile={group.profile}
membershipCount={group.metadata.membershipCount}
@@ -871,12 +905,18 @@ const Main = ({
orgSlug={orgBranding?.slug}
/>
<EventTypeList
types={group.eventTypes}
group={group}
groupIndex={index}
readOnly={group.metadata.readOnly}
/>
{group.eventTypes.length ? (
<EventTypeList
types={group.eventTypes}
group={group}
groupIndex={index}
readOnly={group.metadata.readOnly}
/>
) : group.teamId ? (
<EmptyEventTypeList group={group} />
) : (
<CreateFirstEventTypeView />
)}
</div>
))
)}

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