Compare commits

...
186 Commits
Author SHA1 Message Date
Peer Richelsenandnafees nazik efb9bf7116 replacing intercom with helpscout (#7865)
release.yml / release (push) Canceled after 0s
Co-authored-by: nafees nazik <nafeesnazik21@gmail.com>
2023-03-22 14:29:14 +00:00
49ee5d300b Fix rescheduling updating calendars & duplicate bookings (#7851)
* Fix rescheduling

* Fix tests

* Clean up

* Update packages/core/EventManager.ts

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>

* Commit fix

---------

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-22 15:23:09 +01:00
Peer Richelsen 1512551903 Update package.json 2023-03-22 15:04:02 +01:00
Shane MaglangitandBailey Pumfleet 1b459eb212 style: Increase input width for duration limits (#7779)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-03-22 11:59:03 +00:00
Nafees Nazik e4eb21bf8d fix: metadata get lost when rescheduling (#7873) 2023-03-22 11:17:19 +01:00
79f7a16f62 fix: pass timeFormat to booking page (#7642)
* fix: pass timeFormat to booking page

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

* fix: add zod schema, fix type eroor

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-03-22 15:45:16 +05:30
Hariom Balhara 7fc884912e Make sure that label is also set (#7874) 2023-03-22 04:40:49 +00:00
Hariom Balhara 20b3efbcb6 Handle empty label from Custom inputs (#7871)
* Handle empty label from Custom inputs

* Fix comment
2023-03-21 18:44:18 +00:00
zomars 427b19238d Update yarn.lock 2023-03-21 11:24:08 -07:00
Peer Richelsen 696122a701 updated daily.co env description (#7869) 2023-03-21 18:18:06 +00:00
Shafkathullah IhsanandOmar López 7d9c21cf9a adds fix-availability-slot-addition (#7420)
Co-authored-by: Omar López <zomars@me.com>
2023-03-21 17:56:16 +00:00
Peer Richelsen eb93524f49 Updated Daily.co Partnership links (#7868) 2023-03-21 18:51:30 +01:00
Peer Richelsen 07fd3ad29f i18n for cal video sidebar (#7862) 2023-03-21 23:06:04 +08:00
Hariom Balhara 389633394b Fix/conflicts of custom input Name, with system field (#7856)
* Dont conflict with system fields

* Allow considering a field that has system fields name to be fully editable
2023-03-21 11:47:17 +01:00
Efraín RochínandBailey Pumfleet d27817ad51 Add a minute in endTime working hours when 11:59 is selected (#7852)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-03-21 10:43:36 +00:00
7af23305eb fix: use zod in getting started and settings (#7698)
* fix: use zod in getting started and settings

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

* chore: change name of variable

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-03-21 10:43:07 +00:00
Peer Richelsen db947591ad added team recording hint to cal video (#7855) 2023-03-21 10:39:49 +01:00
Joe Au-Yeung 2ef4a05750 Add team members to iCal, Apple Cal, Outlook, and Lark calendar events (#7846)
* Outlook add team members

* iCal & Apple add team members

* Exchange add team members

* iCal & App cal update include attendees

* Exchange reschedule

* GCal add team members on reschedule

* Lark cal loop through team members
2023-03-21 04:15:59 +01:00
Richard Poelderl f1ac45ee19 Add motif env vars (#7847)
* turn inline logo & shell headers into h3

* turbo (website): add motif.land env vars

* remove $ prefix

* git checkout origin/main packages/embeds/embed-core/src/embed-iframe.ts

* git checkout origin/main packages/prisma/seed-app-store.config.json
2023-03-21 04:15:31 +01:00
zomars 03c9fc7952 Update yarn.lock 2023-03-20 13:03:42 -07:00
Alex van Andel de2a56ebcb v2.7.2 2023-03-20 20:37:31 +01:00
Carina WollendorferandCarinaWolli 0bae688da1 Fixes additional inputs as variables in custom workflow templates (#7840)
* fix additonal inputs as dynamic text variables

* pass responses in same format

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-03-20 20:08:14 +01:00
sean-brydon 7967a65ade Edit location dialog select (#7833)
* Upgrade react-select

* figma 1-1

* Style X

* Fixes timezone select

* Add location select styles

* remove menu is open test
2023-03-20 19:46:22 +01:00
Alex van Andel 1cd96d4c25 No delete of booking on seated events, fix id->uid (#7839)
* No delete of booking on seated events, fix id->uid

* Use uid or id
2023-03-20 18:30:00 +00:00
Peer RichelsenandCarina Wollendorfer 19bc329377 fixed location input, date overwrite dialog (#7778)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-03-20 18:55:40 +01:00
Omar López 0c29f2a135 Adds team members to gCal (#7841) 2023-03-20 17:52:10 +00:00
sean-brydon a145de56ee Adds multiselect checkbox fixes (#7835) 2023-03-20 17:43:32 +01:00
d2352da54b fix: remove cal.com from title when branding is disabled (#7827)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-20 11:41:03 +00:00
Efraín Rochín 5d7685f2c0 revalidate event type page on update (#7685) 2023-03-20 11:21:14 +00:00
ee8b2e67f0 feat: add rate limiting to reset password endpoint (#7700)
* feat: add rate limiting to reset password endpoint

* Update apps/web/pages/api/auth/forgot-password.ts

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Omar López <zomars@me.com>
2023-03-20 11:20:29 +00:00
Miguel Nieto a0c9123ee7 fix: Add appName to generate_api_key_description (#7819) 2023-03-20 11:18:46 +00:00
sean-brydon 131611e114 Upgrade react-select (#7824)
* Upgrade react-select

* figma 1-1

* Style X

* Fixes timezone select
2023-03-20 11:18:23 +00:00
Omar López b3407e19b5 Fixes vercel url usage on the client (#7823) 2023-03-20 11:16:46 +00:00
Nadav Schwartz 0d4b0847d4 Update dialog min-width (#7806) 2023-03-20 10:34:08 +01:00
5d8575abf4 [CAL-1096] Embed selection modal - UI update (#7327)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-20 15:52:54 +08:00
Nafees Nazik 631bf4d9fe fix: hide giphy in cancellation detail page (#7828) 2023-03-20 08:48:22 +01:00
darshan 8fd1d76227 fix add calendar button width and alignment (#7814) 2023-03-19 15:41:56 +01:00
22b189cb19 fix: render markdown in og images (#7693)
* chore: remove unused packages

* fix: add dangerously set inner html

* fix: use markdown

* fix: add remove html regex

* fix: revert dangerouslySetInnerHTML

* fix: title

* added yarn.lock

* yarn

* fix: yarn lock

* Update yarn.lock

* Update yarn lock

* Revert yarn lock changes

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2023-03-19 07:06:16 +00:00
Alex van Andel 27c29c65e2 Bugfix/format without time zone (#7820)
* Format Office365 as non-UTC (defer to timeZone)

* Ensure the start_time is provided in UTC
2023-03-18 22:47:15 +01:00
zomars f0c7d7ff29 Uses internal @calcom/embed-snippet 2023-03-17 16:57:58 -07:00
Richard Poelderl 679965782e chore: update docs links (#7782)
* replace docs.cal.com references with cal.com/docs
2023-03-17 16:42:27 -07:00
Omar López 650ecc7a85 Upgrades turbo to 1.8.3 (#7809) 2023-03-17 21:14:12 +00:00
Joe Au-Yeung 8d27dcdea3 Use dayjs.tz (#7808) 2023-03-17 20:05:22 +00:00
Carina WollendorferandCarinaWolli 6128be8860 Fixes that sometimes last shown slot was unavailable (#7804)
* fix unavailable slots + add relevant test

* code clean up

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-03-17 13:02:56 -07:00
Joe Au-Yeung 13577a5dbe If no expiry date then refresh the token (#7805) 2023-03-17 17:20:41 +00:00
Efraín Rochín 9ba13a3f3c Amend logic to calc slots time already in DST (#7792)
* amend logic to calculate DST time on buildSlots

* send start and end date in UTC

* port the logic to calc DST from buildSlots to inWithinAvailableHours
2023-03-17 12:53:05 +01:00
Sai Deepesh 9ac562cfba feat: add toast dynamically on booking page (#7672) 2023-03-17 16:02:02 +05:30
sean-brydon e1411b3fb2 Fixes select getting darkmode text color even on lightmode (#7799) 2023-03-17 15:34:26 +05:30
Nafees Nazik 0f89415ec3 fix: videoCallUrl is not saved in metadata after rescheduling (#7715) 2023-03-17 13:54:48 +05:30
Nafees Nazik c6ff3dca4a feat: take locale relavant while formatting date in /bookings (#7691)
* feat: use locale while formatting

* fix: tooltip content
2023-03-17 09:12:51 +01:00
Nafees Nazik e001c0b88f test: add multiple guests e2e test (#7774) 2023-03-17 12:33:54 +05:30
zomars 5cbd334a42 Simplifies admin password role checks 2023-03-16 14:02:38 -07:00
Hariom Balhara 470ef737b5 Make embed tests work with single playwright config (#7781)
* Fix Embed test

* Remove only
2023-03-16 11:16:09 -07:00
Joe Au-Yeung f3e8bed1cf Fix dynamic bookings overwriting users (#7699)
* Move sorting dynamic user list

* Remove console logs
2023-03-16 11:12:30 -07:00
zomars 750362c0cf Update yarn lock 2023-03-16 10:44:28 -07:00
zomars 3811dd564d Adds back playwright for OG images generator 2023-03-16 10:42:47 -07:00
Alex van Andel 99fe67b879 v2.7.1
release.yml / release (push) Canceled after 0s
2023-03-16 15:24:19 +00:00
Alex van AndelandKeith Williams 2685e8154f Hotfix/account provider should be lowercase (#7763)
* Fix passThrough console error (passHref is not valid)

* Fix 'use-identity-login' error when logging in with idP

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-03-16 15:12:45 +00:00
4308ea8d52 Fix Google Calendar sharing guest information logic (#7323)
* Fix logic

* Update packages/app-store/googlecalendar/lib/CalendarService.ts

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

* Fixed linting

* Add seats logic to reschedule

---------

Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-03-16 14:50:35 +00:00
Alex van AndelandPeer Richelsen 358aa8be2b Prevent refresh_token nulling if the returned refresh token is empty (#7764)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-16 14:04:30 +00:00
Alex van Andel b20c0df4e8 Remove 'userIsOwner' from isCancelled (#7773) 2023-03-16 13:29:16 +00:00
Nafees NazikandPeer Richelsen 09af14b3f4 fix: badge component line-height according to the DS (#7766)
* fix: badge line-height

* fix: revert padding top and bottom

* fix: remove large prop

* fix: remove large prop

* feat: make md default

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-16 11:51:40 +00:00
77df4117d0 refactor: event types kbar action (#7423)
* refactor: event types action

* chore : wrap actions inside useMemo

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-16 09:49:12 +00:00
Nafees NazikandHariom Balhara 84f7924215 Hotfix: guest are not added to the invite and emails (#7767)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-03-16 14:34:17 +05:30
c8554e6878 [CAL-203] App Store: Add <iframe> as an option in the slider (#7324)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-16 05:30:11 +00:00
Hariom BalharaandAlex van Andel 5993e0842c Fix: EventType crash when Moving up and down beyond boundaries (#7765)
* Make sure that moving up and down doesnt go beyond boundary

* Update packages/features/form-builder/FormBuilder.tsx

Co-authored-by: Alex van Andel <me@alexvanandel.com>

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-16 05:25:37 +00:00
Hariom Balhara 7179b09d7b Allow rescheduleReason to be marked required (#7729) 2023-03-16 10:40:20 +05:30
Nafees Nazik 325dc9805f fix: reschedule reason title in email (#7746)
* fix: reschedule reason title

* fix: remove aditional check
2023-03-16 03:21:59 +00:00
Alex van Andel 0f6a678b74 Disable PRISMA_GENERATE_DATAPROXY 2023-03-16 01:07:02 +00:00
Omar López 96308cd607 v2.7.0
release.yml / release (push) Canceled after 0s
2023-03-15 15:39:45 -07:00
Joe Au-YeungandAlex van Andel 91464fb5e5 Revert - create new bookings - writing team members to DB (#7756)
* Bring back creating team members

* Clean up console.logs

* Push into attendeesData

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-15 15:38:57 -07:00
zomars 49d6dd9802 Updates yarn version 2023-03-15 15:31:22 -07:00
54cefcb16e Upgrades yarn v1 to v3. Improves CI times by 2x (#7738)
* Create env file workflow

* Add env-cache

* Fix env setter

* Fix

* Another fix

* Fix

* Fix

* Fixes

* FFS

* Fix

* Fix

* Fix

* Fix

* Fix

* Cache fixes

* Fixes

* Adds skipping steps

* db-cache fixes

* Test

* Cache fixes

* e2e

* Possible caching conflicts

* Running out of ideas

* Caching is hard

* One more time

* cache-build not skipping

* Fingers crossed

* a

* Test

* Pls

* Please

* LFG

* Build fix

* fix

* Whitespace!!

* Zomars/cal 884 paid events not sending the link (#7318)

* WIP

* Sends correct emails for paid bookings

* Update PaymentService.ts

* Update webhook.ts

* Update webhook.ts

* Update settings back button redirect link (#7403)

* fix(schedule): close on click #7143

* fix(EventSetupTab): validLocations length will always match validLocations length #7138

* fix(SettingsLayout): go back to right route #7379

* feat: get country code from ip geolocation (#6880)

* feat: get coutnry code from ip geolocation

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

* fix: create new api route for fetching code

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

* chore: replace city with country

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

* refactor: create hook for country

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

---------

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

* Team Workflows (#7038)

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* Add destination calendar name to DestinationCalendarSelector (#6701)

* Add destination calendar name

* Type fix

* Search through calendars only for destination calendar credential

* Refactor get connected calendars

* Clean up

---------

Co-authored-by: zomars <zomars@me.com>

* Update viewer.tsx (#7428)

* Fix - add team members to emails (#7207)

* On booking add team members & translation

* Add team members to round robin create

* Only update calendars on reschedule if there is a calendar reference

* Send email on reschedules

* Send team email on cancelled event

* Add team members to calendar event description

* Clean up

* Convert other emails to organizer & teams

* Type check fixes

* More type fixes

* Change organizer scheduled input to an object

* early return updateCalendarEvent

* Introduce team member type

* Fix type errors

* Put team members before attendees

* Remove lodash cloneDeep

* Update packages/core/EventManager.ts

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

* Remove booking select object

* Revert "Remove booking select object"

This reverts commit 9f121ff4ebbaec9a0643111f96c5337b930ddd5b.

* Refactor email manager (#7270)

Co-authored-by: zomars <zomars@me.com>

* Type change

* Remove conditional check for updateAllCalendarEvents

---------

Co-authored-by: zomars <zomars@me.com>

* Typefix

* Updates webhook response

* Update pr.yml

* Update action.yml

* Update action.yml

* Update action.yml

* Update action.yml

* Update action.yml

* Is this redundant?

* Removed setup

* Update action.yml

* Update action.yml

* Consolitades setup step

* Revert "Consolitades setup step"

This reverts commit 5e8d1983cc5ac309fa6875a2fee77aa6287b7151.

* Fix?

* One more time

* Revert "One more time"

This reverts commit fd8b559a135820567f40dbb47a4b4dfc2ef44755.

* Benchmarking buildjet

* Update action.yml

* Re-introduce setup

* Adds embeds to missing pro cache

* Lint fixes

* Adds prettier ignore

* Upgrades to yarn 3

* Updates lockfile

* Reverts CI to ubuntu

* Testing new yarn install

* We cannot use immutable due to our private submodules

* Adds CI skip

* Fixes

* Adds plugin

* Forces local embed package

* Moves eslint to root

* Update yarn.lock

* Playwright fixes

* Embed test fixes

* Splits embed react tests

* Splits embed react tests

* Removes install step to benchmark

* Update playwright.config.ts

* One playwright config for all

* More test fixes

* Update basic.e2e.ts

* Added typescript as a global monorepo dev

* Update to v18

* Update yarn.lock

* Update env-create-file.yml

* Update .github/workflows/pr.yml

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Esaú Morais <55207584+esau-morais@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.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>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-03-15 15:01:04 -07:00
Omar LópezandPeer Richelsen f20d78bec1 Adds prisma data proxy (#7706)
* Adds prisma data proxy

* Fix for pipeline

* Update gh.env

* Update env-create-file.yml

* Reverts some changes

* Update turbo.json

* Update .env.example

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-15 20:33:54 +00:00
zomars 089f52b425 Update yarn.lock 2023-03-15 13:17:43 -07:00
47bf6d27d9 chore: bump node version to v18 (#7310)
* chore: bump node version to v18

* fix(web): support node 19 as accepted

* fix(web): update boxyhq/saml-jackson to 1.8.1

* Drop support for Node 16.x

* Removed n19 pending @azure/msal-node

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: zomars <zomars@me.com>
2023-03-15 20:15:16 +00:00
Richard Poelderl 2cc937ba87 chore: Deactivate automatic link prefetching for /blog links (#7751)
* disable prefetch on the `Tips` cards
2023-03-15 21:08:29 +01:00
Nafees Nazik a57519040e Fix: hydration error /booking/[uid] (#7732) 2023-03-15 21:07:35 +01:00
Nafees Nazik 3924731266 fix: undefined reason (#7745) 2023-03-15 20:07:14 +00:00
Keith Williams 74986bb6a3 Events not being added to outlook (#7726) 2023-03-15 21:04:35 +01:00
Joe Au-Yeung 9568e16ef6 Improvements to teams for self hosters (#7753)
* Write slug when no team billing

* Remove recommended actions for invites
2023-03-15 15:09:43 -04:00
Alex van Andelandzomars e88716758e Remove event type dupes, assign ownership based on existing _user_eventtype.'B' (#7284)
Co-authored-by: zomars <zomars@me.com>
2023-03-15 15:35:54 +00:00
Alex van Andel fcb06a131b Bugfix/allow impersonation non cal id p (#7689)
* Only require a password when IdP is CAL

* Also require twoFactorEnabled
2023-03-15 14:52:32 +00:00
Alex van Andel d778fe6a1b Does case insensitive select, then overwrites email with IdP email (#7673) 2023-03-15 14:52:18 +00:00
Peer Richelsen 877f72d11a fixed leading for labels 2023-03-15 14:04:55 +01:00
f728272c7f Removed email & name placeholder from booking page (#7708)
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-15 15:04:45 +05:30
Omar López 84fd277a97 Adds new responses to zapier payload (#7736) 2023-03-15 14:28:50 +05:30
ae0ed4176b fixes #7548 - Scroll bar because auto height adjustment doesn't take <0.5 values into account (#7549)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-03-15 14:25:11 +05:30
ec4228ab6a feat(app-store): add zohocrm app to app-store (#7182)
* Add zoho-app to the app-store

* Update packages/app-store/zohocrm/api/_getAdd.ts

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

* 1. Remove redundant check for `defaultHandler` 2. Restore yarn.lock`

* update images

* update README with zoho integration

* Fix dirname

* Fix types

* Fix lastname bug

* Fix timezone issue

* Fix eslint warning for unused args

* Revert yarn.lock

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-15 07:50:03 +00:00
zomars 19b1f8a05f Removes Snaplet actions for now 2023-03-14 16:24:42 -07:00
Peer Richelsen 1ba6b08edf cal video: show meeting info in a hideable box (#7295) 2023-03-14 19:43:45 +00:00
Peer Richelsen f027f018ff added screenshot for wipe my cal (#7723) 2023-03-14 13:33:16 +00:00
Udit Takkar 9fd7ca65b5 fix: use title instead of eventtype (#7720) 2023-03-14 13:52:22 +01:00
Nafees NazikandKeith Williams c0f4c8e035 fix: bookings request reschedule UI badge (#7436)
* feat: update badge component

* fix: use the camel cased startIcon prop

* fix: reschedule request badge

* fix: use the camel cased startIcon prop

* fix: remove bold prop

* Removed extra whitespace in HTML

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-03-14 08:34:41 -04:00
ed750c8df1 Seated booking rescheduling. (#5427)
* WIP-already-reschedule-success-emails-missing

* WIP now saving bookingSeatsReferences and identifyin on reschedule/book page

* Remove logs and created test

* WIP saving progress

* Select second slot to pass test

* Delete attendee from event

* Clean up

* Update with main changes

* Fix emails not being sent

* Changed test end url from success to booking

* Remove unused pkg

* Fix new booking reschedule

* remove log

* Renable test

* remove unused pkg

* rename table name

* review changes

* Fix and and other test to reschedule with seats

* Fix api for cancel booking

* Typings

* Update [uid].tsx

* Abstracted common pattern

into maybeGetBookingUidFromSeat

* Reverts

* Nitpicks

* Update handleCancelBooking.ts

* Adds missing cascades

* Improve booking seats changes (#6858)

* Create sendCancelledSeatEmails

* Draft attendee cancelled seat email

* Send no longer attendee email to attendee

* Send email to organizer when attendee cancels

* Pass cloned event data to emails

* Send booked email for first seat

* Add seat reference uid from email link

* Query for seatReferenceUId and add to cancel & reschedule links

* WIP

* Display proper attendee when rescheduling seats

* Remove console.logs

* Only check for already invited when not rescheduling

* WIP sending reschedule email to just single attendee and owner

* Merge branch 'main' into send-email-on-seats-attendee-changes

* Remove console.logs

* Add cloned event to seat emails

* Do not show manage link for calendar event

* First seat, have both attendees on calendar

* WIP refactor booking seats reschedule logic

* WIP Refactor handleSeats

* Change relation of attendee & seat reference to a one-to-one

* Migration with relationship change

* Booking page handling unique seat references

* Abstract to handleSeats

* Remove console.logs and clean up

* New migration file, delete on cascade

* Check if attendee is already a part of the booking

* Move deleting booking logic to `handleSeats`

* When owner reschedule, move whole booking

* Prevent owner from rescheduling if not enough seats

* Add owner reschedule

* Send reschedule email when moving to new timeslot

* Add event data to reschedule email for seats

* Remove DB changes from event manager

* When a booking has no attendees then delete

* Update calendar when merging bookings

* Move both attendees and seat references when merging

* Remove guest list from seats booking page

* Update original booking when moving an attendee

* Delete calendar and video events if no more attendees

* Update or delete integrations when attendees cancel

* Show no longer attendee if a single attendee cancels

* Change booking to accepted if an attendee books on an empty booking

* If booking in same slot then just return the booking

* Clean up

* Clean up

* Remove booking select

* Typos

---------

Co-authored-by: zomars <zomars@me.com>

* Fix migration table name

* Add missing trpc import

* Rename bookingSeatReferences to bookingSeat

* Change relationship between Attendee & BookingSeat to one to one

* Fix some merge conflicts

* Minor merge artifact fixup

* Add the right 'Person' type

* Check on email, less (although still) editable than name

* Removed calEvent.attendeeUniqueId

* rename referenceUId -> referenceUid

* Squashes migrations

* Run cached installs

Should still be faster. Ensures prisma client is up to date.

* Solve attendee form on booking page

* Remove unused code

* Some type fixes

* Squash migrations

* Type fixes

* Fix for reschedule/[uid] redirect

* Fix e2e test

* Solve double declaration of host

* Solve lint errors

* Drop constraint only if exists

* Renamed UId to Uid

* Explicit vs. implicit

* Attempt to work around text flakiness by adding a little break between animations

* Various bugfixes

* Persistently apply seatReferenceUid (#7545)

* Persistently apply seatReferenceUid

* Small ts fix

* Setup guards correctly

* Type fixes

* Fix render 0 in conditional

* Test refactoring

* Fix type on handleSeats

* Fix handle seats conditional

* Fix type inference

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

* Update apps/web/components/booking/pages/BookingPage.tsx

* Fix type and missing logic for reschedule

* Fix delete of calendar event and booking

* Add handleSeats return type

* Fix seats booking creation

* Fall through normal booking for initial booking, handleSeats for secondary/reschedule

* Simplification of fetching booking

* Enable seats for round-robin events

* A lot harder than I expected

* ignore-owner-if-seat-reference-given

* Return seatReferenceUid when second seat

* negate userIsOwner

* Fix booking seats with a link without bookingUid

* Needed a time check otherwise the attendee will be in the older booking

* Can't open dialog twice in test..

* Allow passing the booking ID from the server

* Fixed isCancelled check, fixed test

* Delete through cascade instead of multiple deletes

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Efraín Rochín <roae.85@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-14 04:19:05 +00:00
d545a9e127 Fix save booking title unescaped (#7591)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-03-13 22:03:06 +00:00
zomars 6c55138589 Update crowdin.yml 2023-03-13 11:35:36 -07:00
LucasandPeer Richelsen 18414e857a Fix user time format not respected and booking dates not localized on emails (#7682)
* Localized booking date

* re add space

* Add locale per format call due to lambda dayjs instance share between users

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-13 16:49:48 +00:00
github-actions[bot]andCrowdin Bot 1c8f9cb3fd New Crowdin translations by Github Action (#7675)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-03-13 11:55:43 +01:00
Efraín Rochín 0871314d53 Amend logic to figure out if selected slot is available (#7684) 2023-03-13 09:59:19 +00:00
Peer Richelsenandzomars 02d70c63a5 empty screen for unpublished teams (#7206)
* empty screen for unpublished teams

* i18n for unpublished team, removed contact button, added avatar to EmptyScreen

* Merge branch 'main' into 7202-cal-1114-teamnull-better-404-screen

# Conflicts:
#	apps/web/public/static/locales/en/common.json

* Adds backend logic

* Type fixes

---------

Co-authored-by: zomars <zomars@me.com>
2023-03-12 23:31:55 +00:00
Udit TakkarandPeer Richelsen 71a3374930 fix: add confirm and reject icon in email (#7457)
* fix: add confirm and reject icon in email

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

* fix: remove is_production

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

* wip

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

* style: calltoAction

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

* fix: brightness

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

* fix: styling of button

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-12 17:39:46 +00:00
sean-brydon a22e4eed32 Fixes weird background color (#7678) 2023-03-12 15:54:03 +00:00
Keith Williams 1625f462c2 Fixed linting issues in Playwright config (#7662) 2023-03-12 09:39:08 +01:00
Alex van Andel 5560f9def7 v2.6.12
release.yml / release (push) Canceled after 0s
2023-03-11 20:10:51 +00:00
Alex van Andel 14c38f5108 Small fix to next-auth-options (#7674) 2023-03-11 20:04:42 +00:00
229aeeb0c9 Fix booking time hydration error (#7253)
Root cause of the error is that both server and client try to parse date differently due to localStorage. Since server does not know localStorage when render initial HTML it renders page as if there is no value set for time format.
 On the other hand, client side know the value of time format when it tries to render.

To fix the issue, we force it to parse date with default format at the initial rendering time.

- Add `withDefaultTimeFormat` argument to `parseDate` function
- Add `withDefaultTimeFormat` argument to `parseRecurringDates` function
- Leverage useEffect to dynamically update `withDefaultTimeFormat` arg passed to function

Co-authored-by: sangboak.lee <sangboak.lee@woowahan.com>
Co-authored-by: zomars <zomars@me.com>
2023-03-11 00:24:01 +00:00
Omar López 6a9704349d Revert "Fix conflicts for embed tests (#7311)" (#7670)
This reverts commit 8b47c064ff.
2023-03-10 16:59:58 -07:00
github-actions[bot]andCrowdin Bot d2ffa08c83 New Crowdin translations by Github Action (#7632)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-03-10 16:55:21 -07:00
Lucas Smithandzomars 2317473545 feat(web): improve session retrieval performance (#7584)
* feat(web): improve session retrieval performance

Switch to using `getServerSession` which avoids a HTTP round trip to retrieve session details.

Additionally, migrate deprecated `app/lib/auth` calls to to `@calcom/lib` package.

* fix: update failing test and lint

* Consolidates auth code in features

* Update yarn.lock

* Update packages/trpc/server/createContext.ts

* Oopsie

---------

Co-authored-by: zomars <zomars@me.com>
2023-03-10 23:45:24 +00:00
Lucas SmithandOmar López d81d772cdf feat(lib): add more tests to lib package (#7210)
* feat(lib): add more tests to lib package

Add more tests to the lib package to make it more robust overall. Additionally, tidy any methods that can be modified without changing behaviour and tighten types where possible.

* fix(lib): update missed imports

* fix: revert stylistic changes

* Update getSchedule.test.ts

---------

Co-authored-by: Omar López <zomars@me.com>
2023-03-10 22:10:56 +00:00
cbc9cd60d5 [CAL-988] Limit total appointment time per day/week/month/year (#7166)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-03-10 21:00:19 +01:00
+24 eb66931366 fix(calendar): recurring event fix (#6959)
* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* fix(calendar): recurring event fix

the loop was running way too often on every recurring object
mostly due to the fact that the iterator was not checked for finalization,
and in fact the iterator is not implemented correctly so it does not have done.
instead it checks for undefined or not

Signed-off-by: Tobias Gurtzick <magic@wizardtales.com>

* fix(calendar): recurring started in stoneage
add a start date to the iterator creation

Signed-off-by: Tobias Gurtzick <magic@wizardtales.com>

* fix(calendar): no reason for do here

Signed-off-by: Tobias Gurtzick <magic@wizardtales.com>

* fix(calendar): hour must contain same values as date calendar object...

Signed-off-by: Tobias Gurtzick <magic@wizardtales.com>

* Update yarn.lock

* Adds deployment settings to DB (#6706)

* WIP: SIU: Maximum call stack size exceeded fix? (#6740)

* Maximum call stack size exceeded fix?

# Conflicts:
#	packages/prisma/index.ts

* Prisma client extensions is too much for our TS

* Removed unused Icons

* Type fixes

* replaced edit location dialog subtitle with link to app store (#6746)

* Upgrades typescript to 4.9.4 (#6747)

* Upgrades typescript to 4.9.4

* Type fixes

* Adds booking data to SDK success event (#6737)

* On duplicating an event type, spacing between arrows and label is wide (Length field) (#6717)

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

* Update README.md

* Update README.md

* added blue color for links in bio (#6760)

* removed credits from auth container (#6766)

* On seats, hide attendees if option is selected (#6767)

* On seats, hide attendees if option is selected

* Remove unused line

* Fix shift-enter in bio editor (#6769)

* fix wrong enabled update button at first load

* fix shift-enter error

* small fixes + code clean up

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* fix: back button and heading size (#6768)

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

* Disable autocomplete on password field (#6705)

* fix padding of lenght input on add event modal (#6771)

* Fix: EventType Duplication (#6778)

* fix: location select menu overflowing the container (#6719)

* Update README.md

* adding ukrainian (#6791)

* fix(web): set canonical as `https://cal.com` in the `<HeadSeo/>` (#6693)

* fix(web): set canonical as https://cal.com in the <HeadSeo/>

* use `window.document.location?.href` if self hosted

* clean up

* use `isCalcom` from library

* handle trailing slash on homepage again

* added impersonation tip (#6794)

* Standardize response codes in Login (#6787)

* standardizing login response codes

* playwright fix

* added translation

* fix: booking limits saved as undefined (#6720)

* fix: booking limits saved as undefined

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

* fix: add event limit

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

---------

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

* Correct invalid ukraine language code ua -> uk

* fix(web): remove `noindex` & `nofollow` tags from the `App` and `Shell` components (#6816)

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

* fix: border color in dark teams page (#6817)

* Add additional guests (#6419)

* Add additional guests

* WIP

* WIP

* Type fix

* WIP

* Add validation for already invited attendees

* Revert type to email

* remove controlled fields

* disables signups via env variable (#6212)

* disables signups via env variable

* Apply suggestions from code review

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

* Reusable Upgrade component for teams only features (#6473)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* i18n udpate

* fix: darkmode in booking/[id] page (#6458)

* fix: text-area style

* fix: border styles

* fix: german translation for reschedule booking email  subject (#6483)

* fix: german translation for reschedule subject

* fix: add a full stop

* fix: app store spacing issues (#6481)

* feat: add equal spacing with gap

* fix: title style

* fix: title style

* fix: remove margin

* fix: remove margin

* fix: sub header spacing and slider button

* fix: category card text

* fix: app store spacing issues

* fix: tab in tablet

* fix: shell

* fix: styling of dropdown component (#6440)

* fix: styling of dropdown component

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

* Apply suggestions from code review

* fix: use dropdown item in all pages

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

* chore: undo

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

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

* readding sidebar tips for everyone, added 3 more features to teams empty screen (#6475)

* readding sidebar tips for everyone

* added 3 more features to teams plan

* responsive fixes

* responsive fixes

* small responsive fix

* CTA alignment fixed

* Minor alignment fixes to Shell for event-types/[type]

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* fix: wrong i18n  placeholder in many locales (#6496)

* fix: czech translation

* fix: spanish translation

* fix: italian translation

* fix: japanese translation

* fix: korean translation

* fix: dutch translation

* fix: norwegian translation

* fix: polish translation

* fix: portuguese translation

* fix: portuguese translation

* fix: romanian translation

* fix: russian translation

* fix: serbian translation

* fix: swedish translation

* fix: turkish translation

* fix: ukrainian translation

* fix: vietnamese translation

* fix: chinese simplified translation

* fix: chinese traditional translation

* Disable redirect for cancellation page (#6498)

* [CAL-677] /event-type : Event Setup sidebar item doesn't reflect multiple durations (#6390)

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

* Added fallback to hostsFixed too which is used in COLLECTIVE (#6500)

* Don't update the field when changing the select (#6486)

* Don't update the field when changing the select

* Fixing missing label

* Applied the correct y-8 vertical margin

* Changed the description vertical offset from title a bit more

* Add Trash icon on Availability list dropdown

* fix: tablet sidebar profile being too wide and not centered  (#6490)

* feat: updated app list items styling issues inside settings/conferencing

* feat: fixed tablet sidebar width and not centered issue

* fix: removed un-related changes from this pr

* deleted .env

* fix: added the accidentaly deleted .env file

* fix: removed unwanted .env file from the pr

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

* Bring back PublicShell (#6505)

* Add red text to error message

* WIP

* Rearrange guest input

* Fix validate unique guests logic

* Allow guests on reschedule

* Lint fix

* More lint fixes

* Lint fixes

* More lint fixes

* Fix remove guest button margin

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Jeff Loiselle <jeff@loiselles.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.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: Ronit Panda <72537293+rtpa25@users.noreply.github.com>
Co-authored-by: Alan <alannnc@gmail.com>

* fix: flash of i18n in teams page (#6818)

* Change lint action to run on BuildJet (#6819)

* Feat - invite user to team flow improvements (#6725)

* Fix Dutch custom event names (#6806)

Continued from #6375

* [CAL-727] [500] on /api/trpc/[trpc] (#6795)

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

* update isCalcom for canonicals to handle https://cal.com (#6802)

* update isCalcom for canonicals to handle https://cal.com

* avoid env var

* prettier

* handle window undefined

* handle window undefined during pre-render

* add some logs

* add some logs

* Revert "add some logs"

This reverts commit 9d27de029024e526417bc79e9cee42d7333ac592.

* change useHasTeamPlan hook to useHasPaidPlan (#6699)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* fix: use guest email in create booking link banner (#6694)

* feat: tabs for teams in mobile  (#6685)

* fix: padding

* feat: add teams tab component

* fix: add image to type

* fix: current path check

* fix: add avatar prop

* feat: add teams tab for mobile

* fix: padding

* fix: background and padding x

* fix: empty logic

* fix: conditions

* fix: bg

---------

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

* [CAL-807] /event-types/single - Fix the header (#6738)

* [CAL-807] /event-types/single - Fix the header

* [CAL-807] /event-types/single - Fix the header

* [CAL-807] /event-types/single - Fix the header

---------

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

* Session Timeout (#6439)

* Implementation

* Unifying UI to password screen

* Tweaks for session in password section

* Update apps/web/pages/settings/security/password.tsx

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

* Update packages/features/kbar/Kbar.tsx

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

* Update packages/features/settings/layouts/SettingsLayout.tsx

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

* Relying on extra db query

* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* Adds deployment settings to DB (#6706)

* WIP

* Adds DeploymentTheme

* Add missing migrations

* Adds client extensions for deployment

* Cleanup

* Using same version as other deps

* Reverting prisma changes and fixing things

* Uneeded tx-expect-error

* Fixing default value

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

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

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* Fix embed modal leading inputs (#6822)

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

* Hide attendees from calendar description (#6800)

* Show the organizer timezone when booking on seats

* Only show organizer in event description

* Type fixes

* Update snapshot with new response

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Alan <alannnc@gmail.com>

* fix: installed apps and admin apps layout spacing issues. (#6753)

* feat: add highlight props

* feat: add isTemplate prop

* feat: add invalid credintial prop

* feat: add children prop

* feat: render app list card

* fix: add horizontal gap

* fix: installed app layout style

* fix: admin layout

* feat: add isGlobal to returned data

* feat: fix admin tab.

* use common components list and AppListCard

* show isDefault Badge

* hide switch for global apps

* fix: show switch

* fix: remove isglobal

* fix: layout

* refactor: remove unused component and import

* feat: use app list card component

* fix: query param

* fix: prevent unnecessary props passed to the component

* feat: add disabled style

* feat: add disconnect integration modal

* feat: new changes.

* render disconnect integration model only once.

* add dropdown for actions

* feat: changes in admin apps.

* use dropdown for selecting action buttons.

* use modal for editing keys.

* Remove boolean comparison

---------

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

* user management moved to console.cal.com (#6799)

* Removed unused variables (#6704) (#6750)

* re-adding subtitle to Shell

* lint

* fixed event-type description

* fix: input component style in routing forms (#6731)

* fix: remove custom className

* removed extra type text

removed extra type text from input

---------

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

* Add X-Frame-Options to page headers (#6784)

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

* Removes pay_later_instructions (#6824)

* [CAL-845] Improve the custom event name modal UI/UX (#6660)

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

* webhook from ui design change (#6803)

* webhook from ui change

* fix: webhook test border & test webhook font style

* Fixed broken path to fix SB build failure

* feat: add theming options to teams (#6807)

* /teams improvements (#6831)

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Invalidate hasTeamPlan when the last team is removed (#6837)

* Not allowing random duration (#6743)

* v2.5.7

* fix: padding in shell for mobile (#6825)

* fix: padding

* fix: breakpoints in fab

* fix: padding

* Move e2e to buildjet (#6840)

* i18n fix

* i18n fix

* [CAL-713] /availability single - layout issues. Check spacing, margin, alignment with spec'd design (#6724)

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

* fix: make cal video default value (#6716)

* [CAL-90] Event Types Single / Allow multiple physical locations (#6692)

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

* Don't override but decorate the built-in encode function (#6850)

* Don't override but decorate the built-in encode function

* Removed jose + synced lockfile

* Give highest possible value to z-index (#6853)

* Use trackingId only for trackingDomain as thats the only variable supported (#6856)

* v2.5.8

* fixed availabilty layout

* Left / Right buttons now bottom-aligned and slightly shorter (#6857)

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

* Hotfix: Event Name (#6863) (#6864)

* feat: make key unique (#6861)

* version 2.5.9 (#6868)

* Use Calendar component view=day for drop-in replacement troubleshooter (#6869)

* Use Calendar component view=day for drop-in replacement troubleshooter

* Setting the id to undefined makes the busy time selected

* Updated event title to include title+source

* lots of small changes by me and ciaran (#6871)

* Hotfix: For old Plausible installs enabled in an EventType, give a default value (#6860)

* Add default for trackingId for old plausible installs in event-types

* Fix types

* fix: filter booking in upcoming (#6406)

* fix: filter booking in upcoming

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

* fix: test

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

* fix: wipe-my-cal failing test

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

---------

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

* fix workflows when duplicating event types (#6873)

* fix: get location url from metadata (#6774)

* fix: get location url from metadata

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

* fix: replace location

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

* fix: type error

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

* fix: use zod

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

---------

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

* Updates heroku deployment template (#6879)

* Hide button (#6895)

* Fixed some inconsistencies within single event type page (#6887)

* Fixed some inconsistencies within single event type page

* Fix layout shift on AvailabilityTab

* fix(date-overrides): alignment of edit & delete btns (#6892)

* When unchecking the common schedule, schedule should be nulled (#6898)

* theme for storybook

* nit border change (#6899)

* fix: metadata not saved while creating a booking. (#6866)

* feat: add metadata to booking creation

* fix: bug

* Beginning of Strict CSP Compliance (#6841)

* Add CSP Support and enable it initially for Login page

* Update README

* Make sure that CSP is not enabled if CSP_POLICY isnt set

* Add a new value for x-csp header that tells if instance has opted-in to CSP or not

* Add more src to CSP

* Fix typo in header name

* Remove duplicate headers fn

* Add https://eu.ui-avatars.com/api/

* Add CSP_POLICY to env.example

* v2.5.10

* fix: add req.headers (#6921)

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

* add-console-vars (#6929)

* Admin Wizard Choose License (#6574)

* Implementation

* i18n

* More i18n

* extracted i18n, needs api to get most recent price, added hint: update later

* Fixing i18n var

* Fix booking filters not working for admin (#6576)

* fix: react-select overflow issue in some modals. (#6587)

* feat: add a disable overflow prop

* feat: use the disable overflow prop

* Tailwind Merge (#6596)

* Tailwind Merge

* Fix merge classNames

* [CAL-808] /availability/single - UI issue on buttons beside time inputs (#6561)

* [CAL-808] /availability/single - UI issue on buttons beside time inputs

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

* Update packages/features/schedules/components/Schedule.tsx

* create new translation for tooltip

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

* Bye bye submodules (#6585)

* WIP

* Uses ssh instead

* Update .gitignore

* Update .gitignore

* Update Makefile

* Update git-setup.sh

* Update git-setup.sh

* Replaced Makefile with bash script

* Update package.json

* fix: show button on empty string (#6601)

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

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

* fix: add delete in dropdown (#6599)

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

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

* Update README.md

* Update README.md

* Changed a neutral- classes to gray (#6603)

* Changed a neutral- classes to gray

* Changed all border-1 to border

* Update package.json

* Test fixes

* Yarn lock fixes

* Fix string equality check in git-setup.sh

* [CAL-811] Avatar icon not redirecting user back to the main page (#6586)

* Remove cursor-pointer, remove old Avatar* files

* Fixed styling for checkedSelect + some cleanup

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Harsh/add member invite (#6598)

Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>

* Regenerated lockfile without upgrade (#6610)

* fix: remove annoying outline when <Button /> clicked (#6537)

* fix: remove annoying outline when <Button /> clicked

* Delete yarn.lock

* remove 1 on 1 icon (#6609)

* removed 1-on-1 badge

* changed user to users for group events

* fix: case-sensitivity in apps path (#6552)

* fix: lowercase slug

* fix: make fallback blocking

* Fix FAB (#6611)

* feat: add LocationSelect component (#6571)

* feat: add LocationSelect component

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

* fix: type error

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

* chore: type error

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

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

* Update booking filters design  (#6543)

* Update booking filters

* Add filter on YOUR bookings

* Fix pending members showing up in list

* Reduce the avatar size to 'sm' for now

* Bugfix/dropdown menu trigger as child remove class names (#6614)

* Fix UsernameTextfield to take right height

* Remove className side-effect

* Incorrect resolution version fixed

* Converted mobile DropdownMenuTrigger styles into Button

* v2.5.3

* fix: use items-center (#6618)

* fix tooltip and modal stacking issues (#6491)

* fix tooltip and modal stacking issues

* use z-index in larger screens and less

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Temporary fix (#6626)

* Fix Ga4 tracking (#6630)

* generic <UpgradeScreen> component (#6594)

* first attempt of <UpgradeScreen>

* changes to icons

* reverted changes back to initial state, needs fix: teams not showing

* WIP

* Fix weird reactnode error

* Fix loading text

* added upgradeTip to routing forms

* icon colors

* create and use hook to check if user has team plan

* use useTeamPlan for upgradeTeamsBadge

* replace huge svg with compressed jpeg

* responsive fixes

* Update packages/ui/components/badge/UpgradeTeamsBadge.tsx

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

* Give team plan features to E2E tests

* Allow option to make a user part of team int ests

* Remove flash of paywall for team user

* Add team user for typeform tests as well

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* Removing env var to rely on db

* Restoring i18n keys, set loading moved

* Fixing tailwind-preset glob

* Wizard width fix for md+ screens

* Converting licenses options to radix radio

* Applying feedback + other tweaks

* Reverting this, not this PR related

* Unneeded code removal

* Reverting unneeded style change

* Applying feedback

* Removing licenseType

* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* Adds deployment settings to DB (#6706)

* WIP

* Adds DeploymentTheme

* Add missing migrations

* Adds client extensions for deployment

* Cleanup

* Delete migration.sql

* Relying on both, env var and new model

* Restoring env example doc for backward compat

* Maximum call stack size exceeded fix?

* Revert upgrade

* Update index.ts

* Delete index.ts

* Not exposing license key, fixed radio behavior

* Covering undefined env var

* Self contained checkLicense

* Feedback

* Moar feedback

* Feedback

* Feedback

* Feedback

* Cleanup

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Harsh Singh <51085015+harshsinghatz@users.noreply.github.com>
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>
Co-authored-by: Luis Cadillo <luiscaf3r@gmail.com>
Co-authored-by: Mohammed Cherfaoui <hi@cherfaoui.dev>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* added two new tips (#6915)

* [CAL-488] Timezone selection has a weird double dropdown (#6851)

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

* fix: color and line height of icon (#6913)

* fix: use destination calendar email (#6886)

* fix: use destination calendar email
to display correct primary email

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

* fix: simplify logic

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

---------

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

* fix: dropdown title in bookings page (#6924)

* fixes the broken max size of members on teams page (#6926)

* fix: display provider name instead of url (#6914)

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

* fix: add sortByLabel (#6797)

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

* Email Variables Bug (#6943)

* Remove _subject translations for zh-CN, needs retranslation

* minor timezone-select improvements (#6944)

* fixed timezone select positioning and hover

* fixed timezone select positioning and hover

* Give trackingId a default value because if user doesnt interact with trackingId input it is not set (#6945)

* Block /auth/:path, nothing else. (#6949)

* Block /auth/:path, nothing else.

* Also add /signup

* fix start icon in button (#6950)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* Fixes localisation of {EVENT_DATE} in workflows (#6907)

* translate {EVENT_DATE} variable to correct language

* fix locale for cron schedule reminder emails/sms

* fix type error

* add missing locale to attendees

* fix type error

* code clean up

* revert last commit

* using Intl for date translations

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* Allow account linking for Google and SAML providers (#6874)

* allow account linking for self-hosted instances, both Google and SAML are verified emails

* allow account linking for Google and SSO if emails match with existing username/password account

* Tweaked find user by email since we now have multiple providers (other than credentials provider)

* feat/payment-service-6438-cal-767 (#6677)

* WIP paymentService

* Changes for payment Service

* Fix for stripe payment flow

* Remove logs/comments

* Refactored refund for stripe app

* Move stripe handlePayment to own lib

* Move stripe delete payments to paymentService

* lint fix

* Change handleRefundError as generic function

* remove log

* remove logs

* remove logs

* Return stripe default export to lib/server

* Fixing types

* Fix types

* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* Adds deployment settings to DB (#6706)

* WIP

* Adds DeploymentTheme

* Add missing migrations

* Adds client extensions for deployment

* Cleanup

* Revert "lint fix"

This reverts commit e1a2e4a357e58e6673c47399888ae2e00d1351a6.

* Add validation

* Revert changes removed in force push

* Removing abstract class and just leaving interface implementation

* Fix types for handlePayments

* Fix payment test appStore import

* Fix stripe metadata in event type

* Move migration to separate PR

* Revert "Move migration to separate PR"

This reverts commit 48aa64e0724a522d3cc2fefaaaee5792ee9cd9e6.

* Update packages/prisma/migrations/20230125175109_remove_type_from_payment_and_add_app_relationship/migration.sql

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

---------

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

* Small UI fixes for seats & destination calendars (#6859)

* Do not add former time for events on seats

* Default display destination calendar

* Add seats badge to event type item

* Add string

* Actively watch seats enabled option for requires confirmation

* Only show former time when there is a rescheduleUid

* Update RadioAreaGroup.tsx

* Update CalendarService.ts

* Update CalendarService.ts

---------

Signed-off-by: Tobias Gurtzick <magic@wizardtales.com>
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Tobias Gurtzick <magic@wizardtales.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.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: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Sai Deepesh <saideepesh000@gmail.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: Richard Poelderl <richard.poelderl@gmail.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Jeff Loiselle <jeff@loiselles.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Ronit Panda <72537293+rtpa25@users.noreply.github.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Bram Verstraten <bramverstraten@outlook.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Ben Lam <37053985+blam404@users.noreply.github.com>
Co-authored-by: Josh Avant <joshavant@gmail.com>
Co-authored-by: Joe Shajan <joeshajan1551@gmail.com>
Co-authored-by: Aaron Presley <155617+AaronPresley@users.noreply.github.com>
Co-authored-by: Ben Hybert <53020786+Hybes@users.noreply.github.com>
Co-authored-by: Harsh Singh <51085015+harshsinghatz@users.noreply.github.com>
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>
Co-authored-by: Luis Cadillo <luiscaf3r@gmail.com>
Co-authored-by: Mohammed Cherfaoui <hi@cherfaoui.dev>
Co-authored-by: Deepak Prabhakara <deepak@boxyhq.com>
2023-03-10 10:58:26 -07:00
Alex van Andel 164f8918f5 v2.6.11
release.yml / release (push) Canceled after 0s
2023-03-10 16:54:51 +00:00
Joe Au-Yeung 7b21839c73 Save refresh token (#7661) 2023-03-10 16:42:20 +00:00
Keith Williams f3360e6476 Fix jackson build error (#7641)
* Fixing prettier build issue with jackson

* Refactor to clean up code

* Now using globalThis to reference jackson objects
2023-03-10 15:28:57 +00:00
Nafees Nazik e24f1d8bdb feat: translate attendee scheduled email in bookers locale (#7654) 2023-03-10 14:38:29 +00:00
Carina WollendorferandCarinaWolli d50473468a Delay workflow email cancellation in sendgrid for all case (#7647)
* don't use immediate email cancellation in any case

* use continue

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-03-10 14:28:42 +00:00
Joe Au-Yeung 4e8653cf4e Change to WEBAPP_URL (#7643) 2023-03-10 14:18:03 +00:00
Hariom Balhara 7edebbcef0 Make sure that a hidden field/question is never required (#7652)
* Update required field value as per hidden field value

* Translate Required and Optional

* Translate Label and Placeholder
2023-03-10 14:11:41 +00:00
Hariom Balhara f22bf721f2 Hotfix/Broken EventType duplication (#7655) 2023-03-10 19:12:00 +05:30
Peer Richelsen ba0e443c52 fixed scroll description size, fixed dark mode toggle switch, fixed date height and positioning, fixed date picker container with negative margin (#7656) 2023-03-10 13:02:48 +00:00
Efraín Rochín aebfc7362e set isBefore or isSame to figure out the months between dates (#7649) 2023-03-10 12:24:10 +00:00
Hariom BalharaandPeer Richelsen 6d82e3f1b6 Move routing forms outside of ee and use LicenseRequired component instead. (#7639)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-09 22:58:02 +00:00
dbf74890a5 Only show smsReminderNumber when workflow has ATTENDEE_NUMBER as action (#7567)
* fix smsReminderNumber booking field

* fix sms reminder booking field when updating steps

* code clean up

* use removeSmsReminderFieldForBooking

* Add smsReminderNumber at correct position

* Fix comments

* code clean up

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-03-10 01:09:42 +05:30
Hariom Balhara 95c1c81b3b Fix ColorPicker not showing up and Auto theme not working (#7626) 2023-03-09 11:57:59 -07:00
Peer Richelsen 2305b926cd fixed timezone select (#7638)
release.yml / release (push) Canceled after 0s
2023-03-09 15:23:56 +00:00
d9a555d94a Additional fields as variables for custom event name (#7454)
* feat: add custom validate name util

* refactor: separate custom event type modal into a
different component

* feat: add validation to zod

* chore: add i18n key

* feat: add dynamic imports

* fix: padding

* Omit cache-hit exit 1, assuming it'll fail regardless

* allow custom inputs as variables in event name

* fix ui for adding custom inputs

* show edited event name in modal

* code clean up

* merge fixes

* includes new booking fields logic

* fix event name with custom booking fields

* code clean up

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

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* fix type error

* remove old reqbody variable

---------

Co-authored-by: nafees nazik <nafeesnazik21@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-03-09 15:11:16 +00:00
Miguel Nieto 943142e77f Add google-calendar dependency to Cron app (#7636) 2023-03-09 14:57:05 +00:00
Miguel Nieto 4ea729ca53 feat: Cron app (#7446) 2023-03-09 14:13:12 +00:00
Alex van Andel fa383d671b v2.6.9
release.yml / release (push) Canceled after 0s
2023-03-09 13:45:00 +00:00
Peer Richelsen 77bfd3773b fixed back button in settings (#7634) 2023-03-09 13:44:00 +00:00
Peer RichelsenandAlex van Andel 4cd32fca4a new error for no available user (#7609)
* new error for no available user

* Added cursor-pointer & extra whitespace before 'go back.'

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-09 13:11:07 +00:00
c374f4fa20 Fix: about section editor (#7287)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-09 13:06:37 +00:00
Udit TakkarandPeer Richelsen 2ddada49f9 fix: use /avatar url (#7328)
* fix: use /avatar url

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

* fix: don't fetch avatar

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

* fix: type error

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-09 12:17:13 +00:00
GitStart-Cal.comandgitstart-calcom feb8218e4e Routing form UI update (#7452)
* Routing form UI update

* Routing form UI update

---------

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-03-09 12:14:17 +00:00
Udit Takkar df4fedd61d fix: remove items-center and add fle x shrink (#7624)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-03-09 11:53:26 +00:00
Udit Takkar d80581ad58 fix: prevent refetching country (#7622)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-03-09 10:38:57 +00:00
Umair FarooqandUmair Farooq 65687d6110 Fixed zapier template UI spacing issue (#7582)
* Fixed zapier template UI spacing issue

* Added spacing

---------

Co-authored-by: Umair Farooq <umair.farooq@morressier.com>
2023-03-09 10:22:30 +00:00
Nafees Nazik b3c973e824 fix: deprecated warning (#7620) 2023-03-09 10:19:00 +00:00
Efraín RochínandPeer Richelsen 913466d06d fix/[cal-1091]-Changing the timezone in profile settings doesn't change the date of the booking - only the time (#7598)
* set timezone to date and time displayed on booking details page

* type fixes

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-09 10:18:22 +00:00
Leo Giovanetti 18c1a6a3a2 Fixing sessionTimeout conversion (#7611) 2023-03-09 10:16:19 +00:00
Nafees Nazik fe17203cea fix: select (#7612) 2023-03-09 10:13:39 +00:00
Leo Giovanetti 3a6f05f1e3 Fixing multiple duration not selected correctly (#7597) 2023-03-09 10:12:37 +00:00
Jeroen Reumkens 5ebbd514d8 Added serverside translations to the forgot password page. (#7618) 2023-03-09 09:14:04 +00:00
58b439ca65 Revamp Google Cal warning for Meet, Amie, and Vimcal (#7308)
* Create requires Google Cal component

* Create installed GCal message

* Move requires GCal component to App

* Clean up

* Abstract prerequisite component

* Add requires message on app card

* Refactor to dependency

* Clean up

* Change typeform dep & remove app card dep component

* Clean up

* Change dependency to dependencies

* Pass disableInstall to default install button for AppCard

* Refactor app page to dependencies

* Type fix

* More type fixes

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

* Apply suggestions from code review

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-03-09 10:07:23 +01:00
Udit Takkar c32aadf297 fix: ui inconsistency in calendar app (#7209)
* fix: ui inconsistency in calendar app

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

* fix: remove border

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

* fix: ui bug

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-03-09 00:16:26 +01:00
736c30f134 injects secrets with infisical (#7105)
* injects secrets with infisical

* Update package.json

* Adds infisical project config

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-08 15:28:49 -07:00
github-actions[bot]andCrowdin Bot 7b169ebfc2 New Crowdin translations by Github Action (#7588)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-03-08 23:27:50 +01:00
Alex van Andel 8f5843ab95 Use inviteeTimeZone instead of organizerTimeZone (#7607) 2023-03-08 15:24:11 -07:00
Peer Richelsen 57d17b74c3 Delete issue_label_bot.yaml 2023-03-08 23:04:58 +01:00
Alex van Andelandzomars d40b934866 Bugfix/improved assignment page (#7165)
* Refactor Assignment hosts components

* Addressed NIT

* Remove switch in favour of render object

---------

Co-authored-by: zomars <zomars@me.com>
2023-03-08 15:04:33 -07:00
LucasandPeer Richelsen 2e5c0c6332 Fix/icon size and url (#7605)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-08 21:59:15 +00:00
Peer Richelsen b03bcc7470 Create issue_label_bot.yaml 2023-03-08 22:39:47 +01:00
2da5b19a76 Change quick setup to use docker compose plugin (#7098)
Co-authored-by: Renan <renan.cleyson@moredev.tech>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Omar López <zomars@me.com>
2023-03-08 12:43:39 -07:00
Nafees Nazik 0ee3b26ca8 fix: default conferencing app not showing the correct value after updated (#7589)
* fix: edge case

* feat: add overflow

* fix: invalidate cache
2023-03-08 16:17:08 +00:00
Peer Richelsen e836a042e9 readded overflow-scroll (#7590) 2023-03-08 17:00:59 +01:00
Carina WollendorferandCarinaWolli 37038a0cbf Fixes broken layout of event type description (#7587)
* fix flicker of event type description

* code clean up

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-03-08 15:46:23 +00:00
Omar LópezandAlex van Andel 8b47c064ff Fix conflicts for embed tests (#7311)
* Fix conflicts for embed tests

* Update pr.yml

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-08 13:20:04 +00:00
Alex van Andel 8a2da017ff v2.6.8
release.yml / release (push) Canceled after 0s
2023-03-08 13:04:44 +00:00
Nafees Nazik 2f8280a6ee fix: routing form delete button not rendered (#7424) 2023-03-08 12:39:58 +00:00
4e1e2577eb fix: create a trpc route for fetching country (#7555)
* fix: create a trpc route for fetching country

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

* fix: type error

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-08 12:33:42 +00:00
Udit TakkarandPeer Richelsen 84da528d4d feat: add partstat, rsvp, role (#6940)
* feat: add partstat, rsvp, role

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

* fix: type error

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-08 11:58:09 +00:00
Nafees NazikandPeer Richelsen 29ceaee8d2 feat: Add fresh chat to list of support integration (#7448)
* chore: add fresh chat configs to env.example

* feat: add fresh chat menu item and render

* refactor: remove some event listeners

* chore: make popover closed after the click

* refactor the code and fix some bugs

* feat: auto open chat

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-08 11:30:24 +00:00
Nafees NazikandPeer Richelsen d28c914d3c Fix modal z index issue in settings (#7578)
* feat: add z-index to modal

* feat: reduce z-index

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-08 10:59:06 +00:00
github-actions[bot]andCrowdin Bot 34d3bdf868 New Crowdin translations by Github Action (#7579)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-03-08 11:36:32 +01:00
github-actions[bot]andCrowdin Bot 58836eefbf New Crowdin translations by Github Action (#7577)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-03-08 10:51:07 +01:00
github-actions[bot] 4ab6c2b3da New Crowdin translations by Github Action (#7569) 2023-03-08 08:50:57 +01:00
Efraín Rochínandzomars e1601960df Generate getSchedule response using the invitee timeZone (#7574)
* Generate getSchedule response using the invitee timeZone

* Prevents too much diffing

* Adds follow up to TODO to issue

---------

Co-authored-by: zomars <zomars@me.com>
2023-03-07 19:24:36 -07:00
zomars c97fecf436 Syncs react-hook-form in console/website 2023-03-07 19:08:00 -07:00
Carina WollendorferandCarinaWolli 804ba04983 make eventpage optional (#7573)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-03-07 23:50:34 +00:00
Joe Au-Yeung 163444a0f9 Bug - teams being double created without slugs (#7571) 2023-03-07 23:02:05 +00:00
b0da827483 fix: event location not translated in booking page (#7528)
* fix: event location not translated

* fix: translate

* chore: lowercase trans keys

* fix: label

* feat: snake case labels

* fix: show the link if the user has one

* fix: add translatable keys

* fix:

* chore: fix type error

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Efraín Rochín <roae.85@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-07 22:37:56 +00:00
Alex van Andel 1b468b9ae7 Fixed test (#7572) 2023-03-07 15:15:11 -07:00
2fa83bd512 Add Idp-Initiated SSO (#6781)
* wip idp enabled login

* add route to handle callback from IdP

* update the new provider

* cleanup

* fix the type

* add suggested changes

* make the suggested changes

* use client secret verifier

* Make [...nextauth] a little easier to read

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-03-07 14:31:39 -07:00
zomars 7eecc311e0 Syncs yarn.lock with submodules 2023-03-07 12:55:44 -07:00
github-actions[bot]andCrowdin Bot 6c8b428577 New Crowdin translations by Github Action (#7566)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-03-07 20:04:35 +01:00
github-actions[bot]andCrowdin Bot 699c55da18 New Crowdin translations by Github Action (#7562)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-03-07 19:48:20 +01:00
Kiran KandPeer Richelsen 74a30a180e URL to initiate SAML authentication flow (#6813)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-07 18:33:16 +00:00
Hariom BalharaandPeer Richelsen ce8e1d52da Fix "User Added Question" Label in Email and Calendar Invite (#7559)
* Show label in email and calendar invite and send label as well as name in webhook

* Make common code reusable

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-07 17:50:54 +00:00
Hariom Balhara 6f8ea490d0 "Manage Booking Questions" - Add a comprehensive test (#7465)
* Add first test

* Add test for team event as well
2023-03-07 17:40:47 +00:00
github-actions[bot]andCrowdin Bot a2fd5ba2a2 New Crowdin translations by Github Action (#7561)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-03-07 18:28:10 +01:00
Udit Takkar 48c8b749ce fix: remove alert and disable toggle (#7542) 2023-03-07 17:05:01 +00:00
Syed Ali Shahbaz faaf935724 Fix for bad primary calendar check (#7557) 2023-03-07 15:47:10 +00:00
github-actions[bot]andCrowdin Bot 0c94b0a30a New Crowdin translations by Github Action (#7546)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-03-07 12:25:32 +01:00
Nafees Nazik a25195cf20 fix: overflow issue in event type form builder and booking page (#7553) 2023-03-07 13:25:06 +05:30
517 changed files with 51758 additions and 33239 deletions
+13 -1
View File
@@ -15,16 +15,22 @@
# - LARK
# - WEB3
# - SALESFORCE
# - ZOHOCRM
# - APP STORE **********************************************************************************************
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
# - DAILY.CO VIDEO
# Enables Cal Video. to get your key
# 1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
# 2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
# @see https://github.com/calcom/cal.com#obtaining-daily-api-credentials
DAILY_API_KEY=
DAILY_SCALE_PLAN=''
# - GOOGLE CALENDAR/MEET/LOGIN
# Needed to enable Google Calendar integration and Login with Google
# @see https://github.com/calendso/calendso#obtaining-the-google-api-credentials
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
GOOGLE_API_CREDENTIALS=
# To enable Login with Google you need to:
@@ -109,4 +115,10 @@ INFURA_API_KEY=""
# Used for the Salesforce (Sales Cloud) app
SALESFORCE_CONSUMER_KEY=""
SALESFORCE_CONSUMER_SECRET=""
# - ZOHOCRM
# Used for the Zoho CRM integration
ZOHOCRM_CLIENT_ID=""
ZOHOCRM_CLIENT_SECRET=""
# *********************************************************************************************************
+13
View File
@@ -22,6 +22,13 @@ CALCOM_LICENSE_KEY=
# - DATABASE ************************************************************************************************
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
# PRISMA_GENERATE_DATAPROXY=true
PRISMA_GENERATE_DATAPROXY=
# ***********************************************************************************************************
# - SHARED **************************************************************************************************
@@ -39,6 +46,8 @@ SAML_DATABASE_URL=
SAML_ADMINS=
# NEXT_PUBLIC_HOSTED_CAL_FEATURES=1
NEXT_PUBLIC_HOSTED_CAL_FEATURES=
# For additional security set to a random secret and use that value as the client_secret during the OAuth 2.0 flow.
SAML_CLIENT_SECRET_VERIFIER=
# If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line.
# @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
@@ -76,6 +85,10 @@ NEXT_PUBLIC_ZENDESK_KEY=
# Help Scout Config
NEXT_PUBLIC_HELPSCOUT_KEY=
# Fresh Chat Config
NEXT_PUBLIC_FRESHCHAT_TOKEN=
NEXT_PUBLIC_FRESHCHAT_HOST=
# Inbox to send user feedback
SEND_FEEDBACK_EMAIL=
+6
View File
@@ -0,0 +1,6 @@
node_modules
**/**/node_modules
**/**/.next
**/**/public
packages/prisma/zod
apps/web/public/embed
+1 -1
View File
@@ -30,4 +30,4 @@ packages/features/ee/workflows @CarinaWolli @calcom/core
packages/features/tips @PeerRich @calcom/core
packages/ui @calcom/ui
packages/config @calcom/core
packages/app-store/ee/routing-forms @hariombalhara @calcom/core
packages/app-store/routing-forms @hariombalhara @calcom/core
+2 -1
View File
@@ -3,7 +3,7 @@ description: "Cache or restore if necessary"
inputs:
node_version:
required: false
default: v16.x
default: v18.x
runs:
using: "composite"
steps:
@@ -20,6 +20,7 @@ runs:
with:
path: |
${{ github.workspace }}/apps/web/.next
${{ github.workspace }}/apps/web/public/embed
**/.turbo/**
**/dist/**
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
+49 -6
View File
@@ -1,9 +1,21 @@
name: Yarn install
description: "Install all NPM dependencies, caches them and restores if necessary"
########################################################################################
# "yarn install" composite action for yarn 2/3/4+ and "nodeLinker: node-modules" #
#--------------------------------------------------------------------------------------#
# Cache: #
# - Downloaded zip archive (multi-arch, preserved across yarn.lock changes) #
# - Yarn install state (discarded on yarn.lock changes) #
# References: #
# - bench: https://gist.github.com/belgattitude/0ecd26155b47e7be1be6163ecfbb0f0b #
# - vs @setup/node: https://github.com/actions/setup-node/issues/325 #
########################################################################################
name: "Yarn install"
description: "Run yarn install with node_modules linker and cache enabled"
inputs:
node_version:
required: false
default: v16.x
default: v18.x
runs:
using: "composite"
steps:
@@ -11,9 +23,40 @@ runs:
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node_version }}
cache: "yarn"
- name: Yarn install
- name: Expose yarn config as "$GITHUB_OUTPUT"
id: yarn-config
shell: bash
run: |
yarn install --prefer-offline --frozen-lockfile
echo "CACHE_FOLDER=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
# Yarn rotates the downloaded cache archives, @see https://github.com/actions/setup-node/issues/325
# Yarn cache is also reusable between arch and os.
- name: Restore yarn cache
uses: actions/cache@v3
id: yarn-download-cache
with:
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
key: yarn-download-cache-${{ hashFiles('yarn.lock') }}
restore-keys: |
yarn-download-cache-
# Invalidated on yarn.lock changes
- name: Restore yarn install state
id: yarn-install-state-cache
uses: actions/cache@v3
with:
path: .yarn/ci-cache/
key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
- name: Install dependencies
shell: bash
run: |
yarn install --inline-builds
yarn prisma generate
env:
# CI optimizations. Overrides yarnrc.yml options (or their defaults) in the CI action.
YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # So it doesn't try to remove our private submodule deps
YARN_ENABLE_GLOBAL_CACHE: "false" # Use local cache folder to keep downloaded archives
YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz # Very small speedup when lock does not change
# Other environment variables
HUSKY: "0" # By default do not run HUSKY install
@@ -1,20 +0,0 @@
name: Cleanup Preview Databases
env:
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
SNAPLET_PROJECT_ID: cl4u26bwz7962859ply7ibuo43t
on:
pull_request:
types: [closed]
branches:
- main
jobs:
delete:
runs-on: ubuntu-latest
steps:
- uses: snaplet/action@v1
if: ${{ env.SNAPLET_ACCESS_TOKEN && env.SNAPLET_PROJECT_ID }}
with:
delete: true
-2
View File
@@ -18,5 +18,3 @@ jobs:
days-before-stale: 60
include-only-assigned: true
days-before-close: -1
+6 -3
View File
@@ -17,11 +17,14 @@ jobs:
uses: actions/checkout@v3
- name: crowdin action
uses: crowdin/github-action@1.4.9
uses: crowdin/github-action@1.5.1
with:
upload_translations: true
upload_sources: true
download_translations: true
pull_request_labels: "♻️ autoupdate, automerge"
push_translations: true
commit_message: "New Crowdin translations by Github Action"
localization_branch_name: main
create_pull_request: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
-37
View File
@@ -1,37 +0,0 @@
# .github/workflows/preview.yml
name: Preview Environment
env:
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
SNAPLET_PROJECT_ID: ${{ secrets.SNAPLET_PROJECT_ID }}
VERCEL_ACCESS_TOKEN: ${{ secrets.VERCEL_ACCESS_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
on:
pull_request:
types: [opened, synchronize, closed]
branches:
- main
jobs:
db-preview-deploy:
if: ${{ github.event.action == 'opened' || github.event.action == 'synchronize' }}
runs-on: ubuntu-latest
steps:
- id: snaplet
uses: snaplet/action@v1
- uses: snaplet/vercel-action@v1
with:
env: |
DATABASE_URL=${{ steps.snaplet.outputs.database-url }}
db-preview-delete:
if: ${{ github.event.action == 'closed' }}
runs-on: ubuntu-latest
steps:
- uses: snaplet/action@v1
with:
delete: true
- uses: snaplet/vercel-action@v1
with:
delete: true
-18
View File
@@ -1,18 +0,0 @@
name: Staging DB Snapshot
on:
schedule:
- cron: "0 4 * * 1-5" # At 04:00 on every day-of-week from Monday through Friday.
workflow_dispatch:
jobs:
db-staging-snapshot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Snaplet CLI
run: curl -sL https://app.snaplet.dev/get-cli/ | bash
- name: Restore Snapshot
run: snaplet snapshot restore -y
env:
SNAPLET_DATABASE_URL: ${{ secrets.SNAPLET_DATA_TARGET_DB_URL }}
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
SNAPLET_DATABASE_ID: ${{ secrets.SNAPLET_DATABASE_ID }}
+36
View File
@@ -0,0 +1,36 @@
name: E2E Embed tests and booking flow(for non-embed as well)
on:
workflow_call:
jobs:
e2e-embed:
timeout-minutes: 20
runs-on: ubuntu-latest
services:
postgres:
image: postgres:12.1
env:
POSTGRES_USER: postgres
POSTGRES_DB: calendso
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- uses: ./.github/actions/env-read-file
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn test-e2e:embed-react
env:
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results
path: test-results
+4 -11
View File
@@ -25,19 +25,12 @@ jobs:
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn turbo run embed-tests-update-snapshots:ci --scope=@calcom/embed-react --concurrency=1
run: yarn test-e2e:embed
env:
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
- name: Upload embed-core results
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results-embed-core
path: packages/embeds/embed-core/playwright/results
- name: Upload embed-react results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results-embed-react
path: packages/embeds/embed-react/playwright/results
name: test-results
path: test-results
+2 -1
View File
@@ -20,7 +20,7 @@ env:
INPUT_ENV_PAYMENT_FEE_FIXED: 10
INPUT_ENV_SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
INPUT_ENV_SAML_ADMINS: pro@example.com
INPUT_ENV_NEXTAUTH_URL: http://localhost:3000/api/auth
INPUT_ENV_NEXTAUTH_URL: http://127.0.0.1:3000/api/auth
INPUT_ENV_NEXT_PUBLIC_IS_E2E: 1
# INPUT_ENV_EMAIL_FROM: e2e@cal.com
# INPUT_ENV_EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
@@ -39,6 +39,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ozaytsev86/create-env-file@v1
with:
file-name: ${{ github.workspace }}/gh.env
+11
View File
@@ -21,6 +21,7 @@ jobs:
outputs:
app-store: ${{ steps.filter.outputs.app-store }}
embed: ${{ steps.filter.outputs.embed }}
embed-react: ${{ steps.filter.outputs.embed-react }}
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
@@ -34,6 +35,9 @@ jobs:
embed:
- 'apps/web/**'
- 'packages/embeds/**'
embed-react:
- 'apps/web/**'
- 'packages/embeds/**'
env:
name: Create env file
@@ -81,6 +85,13 @@ jobs:
uses: ./.github/workflows/e2e-embed.yml
secrets: inherit
e2e-embed-react:
name: E2E React embeds tests
if: ${{ needs.changes.outputs.embed-react == 'true' }}
needs: [changes, lint, build]
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
analyze:
needs: build
uses: ./.github/workflows/nextjs-bundle-analysis.yml
+9 -3
View File
@@ -77,9 +77,6 @@ lint-results
#Storybook
apps/storybook/build-storybook.log
# Infisical
.infisical.json
# Snaplet
.snaplet/snapshots
.snaplet/structure.d.ts
@@ -90,3 +87,12 @@ apps/api
apps/website
apps/console
apps/auth
# Yarn Modern
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+8
View File
@@ -0,0 +1,8 @@
{
"workspaceId": "63ea8121b6e2b0543ba79616",
"defaultEnvironment": "dev",
"gitBranchToEnvironmentMapping": {
"main": "staging",
"production": "prod"
}
}
+1 -1
View File
@@ -1 +1 @@
v16
v18
+1
View File
@@ -14,3 +14,4 @@ public
.DS_Store
.eslintignore
packages/prisma/zod
apps/web/public/embed
File diff suppressed because one or more lines are too long
+873
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.4.1.cjs
+1 -2
View File
@@ -4,7 +4,7 @@ Contributions are what make the open source community such an amazing place to l
- 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.
## Priorities
## Priorities
<table>
<tr>
@@ -57,7 +57,6 @@ Contributions are what make the open source community such an amazing place to l
</tr>
</table>
## Developing
The development branch is `main`. This is the branch that all pull
+17 -4
View File
@@ -88,6 +88,7 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
- [React](https://reactjs.org/?ref=cal.com)
- [Tailwind](https://tailwindcss.com/?ref=cal.com)
- [Prisma](https://prisma.io/?ref=cal.com)
- [Daily.co](https://go.cal.com/daily)
## Stay Up-to-Date
@@ -350,17 +351,16 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
</picture>
</a>
<!-- CONTRIBUTORS -->
### Contributors
<a href="https://github.com/calcom/cal.com/graphs/contributors">
<img src="https://contrib.rocks/image?repo=calcom/cal.com" />
</a>
<!-- TRANSLATIONS -->
### Translations
@@ -433,7 +433,7 @@ following
### Obtaining Daily API Credentials
1. Open [Daily](https://www.daily.co/) and sign into your account.
1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
3. Copy your API key.
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
@@ -452,6 +452,19 @@ following
9. Click the "Save" button at the bottom footer.
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
### Obtaining ZohoCRM Client ID and Secret
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
2. From within the API console page, go to "Applications".
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
4. Fill in any information you want in the "Client Details" tab
5. Go to tab "Client Secret" tab.
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
9. Click the "Save"/ "UPDATE" button at the bottom footer.
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
## Workflows
### Setting up SendGrid for Email reminders
+1 -1
View File
@@ -1 +1 @@
14.17
v18
+1 -2
View File
@@ -24,7 +24,6 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@calcom/config": "*",
"eslint": "^8.34.0"
"@calcom/config": "*"
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
node_modules
prisma/zod
public/embed
+1 -1
View File
@@ -1 +1 @@
v16
v18
+1
View File
@@ -0,0 +1 @@
public/embed
+1 -1
View File
@@ -64,7 +64,7 @@ export default function AppListCard(props: AppListCardProps) {
return (
<div className={`${highlight ? "bg-yellow-100" : ""}`}>
<div className="flex gap-x-3 p-4">
<div className="flex gap-x-3 px-5 py-4">
{logo ? <img className="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">
@@ -94,16 +94,6 @@ const DestinationCalendarSelector = ({
styles={{
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
option: (defaultStyles, state) => ({
...defaultStyles,
backgroundColor: state.isSelected
? state.isFocused
? "var(--brand-color)"
: "var(--brand-color)"
: state.isFocused
? "var(--brand-color-dark-mode)"
: "var(--brand-text-color)",
}),
control: (defaultStyles) => {
return {
...defaultStyles,
+88 -76
View File
@@ -327,10 +327,10 @@ const embeds: {
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="2" fill="#E1E1E1" />
<rect x="48" y="86.5" width="48" height="4" rx="1" fill="#E1E1E1" />
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="6" fill="#F3F4F6" />
<rect x="48" y="86.5" width="48" height="4" rx="6" fill="#F3F4F6" />
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
@@ -378,9 +378,9 @@ const embeds: {
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="24.5" y="51" width="139" height="163" rx="1.5" stroke="#292929" />
<rect x="176" y="50.5" width="108" height="164" rx="2" fill="#E1E1E1" />
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="24.5" y="51" width="139" height="163" rx="6" stroke="#292929" />
<rect x="176" y="50.5" width="108" height="164" rx="6" fill="#F3F4F6" />
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
</svg>
),
},
@@ -400,12 +400,12 @@ const embeds: {
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="226" y="223.5" width="66" height="26" rx="2" fill="#292929" />
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
<rect x="24" y="50.5" width="120" height="76" rx="6" fill="#F3F4F6" />
<rect x="24" y="138.5" width="120" height="76" rx="6" fill="#F3F4F6" />
<rect x="156" y="50.5" width="128" height="164" rx="6" fill="#F3F4F6" />
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
<rect x="226" y="223.5" width="66" height="26" rx="6" fill="#292929" />
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
</svg>
),
@@ -426,62 +426,65 @@ const embeds: {
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" fill="#F8F8F8" />
<rect opacity="0.8" x="108" y="85" width="80" height="8" rx="2" fill="#E1E1E1" />
<rect x="108" y="97" width="48" height="4" rx="1" fill="#E1E1E1" />
<rect x="109" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="110" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="133" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="169" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="181" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="169" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="181" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="136" width="6" height="6" rx="1" fill="#3E3E3E" />
<path
d="M121 135H127V133H121V135ZM128 136V142H130V136H128ZM127 143H121V145H127V143ZM120 142V136H118V142H120ZM121 143C120.448 143 120 142.552 120 142H118C118 143.657 119.343 145 121 145V143ZM128 142C128 142.552 127.552 143 127 143V145C128.657 145 130 143.657 130 142H128ZM127 135C127.552 135 128 135.448 128 136H130C130 134.343 128.657 133 127 133V135ZM121 133C119.343 133 118 134.343 118 136H120C120 135.448 120.448 135 121 135V133Z"
fill="#3E3E3E"
/>
<rect x="133" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="169" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="181" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="169" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="181" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="169" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="181" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="172" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="169" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" stroke="#292929" />
<rect x="24" y="0.50293" width="260" height="24" rx="6" fill="#F3F4F6" />
<rect x="24" y="35" width="259" height="192" rx="5.5" fill="#F9FAFB" />
<g filter="url(#filter0_i_3223_14162)">
<rect opacity="0.8" x="40" y="99" width="24" height="24" rx="2" fill="#E5E7EB" />
<rect x="40" y="127" width="48" height="8" rx="1" fill="#E5E7EB" />
<rect x="40" y="139" width="82" height="8" rx="1" fill="#E5E7EB" />
<rect x="40" y="151" width="34" height="4" rx="1" fill="#E5E7EB" />
<rect x="40" y="159" width="34" height="4" rx="1" fill="#E5E7EB" />
</g>
<rect x="152" y="48" width="2" height="169" rx="2" fill="#E5E7EB" />
<rect opacity="0.8" x="176" y="84" width="80" height="8" rx="2" fill="#E5E7EB" />
<rect x="176" y="96" width="48" height="4" rx="1" fill="#E5E7EB" />
<rect x="177" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="109" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="201" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="237" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="249" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="237" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="249" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="135" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="187.3" y="133.4" width="9" height="9" rx="1.5" stroke="#0D121D" />
<rect x="201" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="237" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="249" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="201" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="147" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="237" y="147" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="249" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="201" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="159" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="237" y="159" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="249" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="201" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="171" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="237" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="24" y="35" width="259" height="192" rx="5.5" stroke="#101010" />
<rect x="24" y="241.503" width="260" height="24" rx="6" fill="#F3F4F6" />
</svg>
),
},
@@ -629,9 +632,9 @@ const ChooseEmbedTypesDialogContent = () => {
const { t } = useLocale();
const router = useRouter();
return (
<DialogContent type="creation" size="lg">
<div className="mb-4">
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
<div className="mb-2">
<h3 className="mb-2 text-2xl font-bold leading-none text-gray-900" id="modal-title">
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
</h3>
<div>
@@ -641,7 +644,7 @@ const ChooseEmbedTypesDialogContent = () => {
<div className="flex items-start">
{embeds.map((embed, index) => (
<button
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-gray-100 ltr:mr-2 rtl:ml-2"
className="hover:border-primary-900 w-1/3 rounded-md border border-gray-200 bg-gray-50 p-6 text-left hover:rounded-md hover:bg-gray-100 ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0"
key={index}
data-testid={embed.type}
onClick={() => {
@@ -649,11 +652,11 @@ const ChooseEmbedTypesDialogContent = () => {
embedType: embed.type,
});
}}>
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
<div className="order-none box-border flex-none rounded-md border border-solid bg-white">
{embed.illustration}
</div>
<div className="mt-2 font-medium text-gray-900">{embed.title}</div>
<p className="text-sm text-gray-500">{embed.subtitle}</p>
<div className="mt-4 font-semibold leading-none text-gray-900">{embed.title}</div>
<p className="mt-2 text-sm text-gray-500">{embed.subtitle}</p>
</button>
))}
</div>
@@ -669,8 +672,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
embedUrl: string;
}) => {
const { t } = useLocale();
const router = useRouter();
const iframeRef = useRef<HTMLIFrameElement>(null);
const dialogContentRef = useRef<HTMLDivElement>(null);
const s = (href: string) => {
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
const [a, b] = href.split("=");
@@ -809,7 +815,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
];
return (
<DialogContent size="xl" className="p-0.5" type="creation">
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation">
<div className="flex">
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
@@ -960,6 +966,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div>Button Color</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
@@ -979,6 +987,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div>Text Color</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
@@ -1060,6 +1070,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div className="mb-2 pt-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
+50 -15
View File
@@ -1,9 +1,10 @@
import Link from "next/link";
import { useRouter } from "next/router";
import type { IframeHTMLAttributes } from "react";
import React, { useState } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton } from "@calcom/app-store/components";
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
import Shell from "@calcom/features/shell/Shell";
@@ -24,6 +25,8 @@ import {
FiShield,
} from "@calcom/ui/components/icon";
/* These app slugs all require Google Cal to be installed */
const Component = ({
name,
type,
@@ -43,11 +46,12 @@ const Component = ({
tos,
privacy,
isProOnly,
images,
descriptionItems,
isTemplate,
dependencies,
}: Parameters<typeof App>[0]) => {
const { t } = useLocale();
const hasImages = images && images.length > 0;
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
const router = useRouter();
const mutation = useAddAppMutation(null, {
@@ -76,23 +80,40 @@ const Component = ({
}
);
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
enabled: !!dependencies,
});
const disableInstall =
dependencyData.data && dependencyData.data.some((dependency) => !dependency.installed);
// const disableInstall = requiresGCal && !gCalInstalled.data;
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
// Such apps, can only be installed once.
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
return (
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
{hasImages && (
{hasDescriptionItems && (
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
{images ? (
images.map((img) => (
<img
key={img}
src={img}
alt={`Screenshot of app ${name}`}
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
/>
))
{descriptionItems ? (
descriptionItems.map((descriptionItem, index) =>
typeof descriptionItem === "object" ? (
<div
key={`iframe-${index}`}
className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full">
<iframe allowFullScreen {...descriptionItem.iframe} />
</div>
) : (
<img
key={descriptionItem}
src={descriptionItem}
alt={`Screenshot of app ${name}`}
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
/>
)
)
) : (
<SkeletonText />
)}
@@ -101,7 +122,7 @@ const Component = ({
<div
className={classNames(
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
hasImages && "lg:ml-8"
hasDescriptionItems && "lg:ml-8"
)}>
<div className="mb-8 flex pt-4">
<header>
@@ -137,6 +158,7 @@ const Component = ({
<InstallAppButton
type={type}
isProOnly={isProOnly}
disableInstall={disableInstall}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
@@ -176,6 +198,7 @@ const Component = ({
<InstallAppButton
type={type}
isProOnly={isProOnly}
disableInstall={disableInstall}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
@@ -203,6 +226,16 @@ const Component = ({
) : (
<SkeletonButton className="h-10 w-24" />
)}
{dependencies &&
(!dependencyData.isLoading ? (
<div className="mt-6">
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
</div>
) : (
<SkeletonButton className="mt-6 h-20 grow" />
))}
{price !== 0 && (
<span className="block text-right">
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
@@ -330,8 +363,10 @@ export default function App(props: {
privacy?: string;
licenseRequired: AppType["licenseRequired"];
isProOnly: AppType["isProOnly"];
images?: string[];
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
isTemplate?: boolean;
disableInstall?: boolean;
dependencies?: string[];
}) {
return (
<Shell smallHeading isPublic heading={<ShellHeading />} backPath="/apps" withoutSeo>
@@ -88,7 +88,7 @@ function CalendarSwitch(props: {
}
);
return (
<div className="flex py-1">
<div className="flex flex-col py-1 sm:flex-row">
<Switch
key={props.externalId}
name="enabled"
@@ -99,7 +99,7 @@ function CalendarSwitch(props: {
}}
/>
{!!props.destination && (
<span className="ml-4 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800">
<span className="ml-8 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800 sm:ml-4">
<FiArrowLeft className="h-4 w-4" />
{t("adding_events_to")}
</span>
@@ -184,8 +184,8 @@ function ConnectedCalendarsList(props: Props) {
<div className="border-t border-gray-200">
{!fromOnboarding && (
<>
<p className="px-4 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
<ul className="space-y-2 p-4">
<p className="px-5 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
<ul className="space-y-2 px-5 py-4">
{item.calendars.map((cal) => (
<CalendarSwitch
key={cal.externalId}
@@ -264,7 +264,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
<>
{heading && (
<div className="flex flex-col gap-6 rounded-md border border-gray-200 p-7">
<div className="flex flex-col gap-6 rounded-md border p-7">
<ShellSubHeading
title={t("calendar")}
subtitle={t("installed_app_calendar_description")}
@@ -96,16 +96,6 @@ const DestinationCalendarSelector = ({
styles={{
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
option: (defaultStyles, state) => ({
...defaultStyles,
backgroundColor: state.isSelected
? state.isFocused
? "var(--brand-color)"
: "var(--brand-color)"
: state.isFocused
? "var(--brand-color-dark-mode)"
: "var(--brand-text-color)",
}),
control: (defaultStyles) => {
return {
...defaultStyles,
@@ -1,3 +1,5 @@
import { z } from "zod";
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -17,10 +19,27 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
// It's possible that the location app got uninstalled
return null;
}
const translateAbleKeys = [
"attendee_in_person",
"in_person",
"attendee_phone_number",
"link_meeting",
"organizer_phone_number",
];
const locationKey = z.string().default("").parse(locationKeyToString(location));
const translatedLocation = location.type.startsWith("integrations:")
? eventLocationType.label
: translateAbleKeys.includes(locationKey)
? t(locationKey)
: locationKey;
return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? (
<FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
) : (
<img
src={eventLocationType.iconUrl}
@@ -31,14 +50,12 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
alt={`${eventLocationType.label} icon`}
/>
)}
<Tooltip content={t(locationKeyToString(location) ?? "")}>
<p className="line-clamp-1">{t(locationKeyToString(location) ?? "")}</p>
<Tooltip content={translatedLocation}>
<p className="line-clamp-1">{translatedLocation}</p>
</Tooltip>
</div>
);
})}
</div>
) : (
<></>
);
) : null;
}
+22 -7
View File
@@ -24,6 +24,7 @@ type AvailableTimesProps = {
eventTypeSlug: string;
date?: Dayjs;
seatsPerTimeSlot?: number | null;
bookingAttendees?: number | null;
slots?: Slot[];
isLoading: boolean;
ethSignature?: string;
@@ -39,6 +40,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
timeFormat,
onTimeFormatChange,
seatsPerTimeSlot,
bookingAttendees,
ethSignature,
}) => {
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
@@ -64,13 +66,13 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
return (
<div ref={slotPickerRef}>
{!!date ? (
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
<div className="mb-6 flex items-center text-left text-base">
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
<div className="mb-4 flex items-center text-left text-base">
<div className="mr-4">
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span>
<span className="text-bookinglight font-medium">
<span className="text-bookinglight">
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
</span>
</div>
@@ -85,12 +87,14 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
/>
</div>
</div>
<div ref={ref} className="flex-grow overflow-y-auto sm:block md:h-[364px]">
<div
ref={ref}
className="scroll-bar scrollbar-track-w-20 relative -mb-4 flex-grow overflow-y-auto sm:block md:h-[364px]">
{slots.length > 0 &&
slots.map((slot) => {
type BookingURL = {
pathname: string;
query: Record<string, string | number | string[] | undefined>;
query: Record<string, string | number | string[] | undefined | TimeFormat>;
};
const bookingUrl: BookingURL = {
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
@@ -99,6 +103,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
date: dayjs.utc(slot.time).tz(timeZone()).format(),
type: eventTypeId,
slug: eventTypeSlug,
timeFormat,
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
...(ethSignature ? { ethSignature } : {}),
@@ -114,18 +119,28 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
bookingUrl.query.bookingUid = slot.bookingUid;
}
let slotFull, notEnoughSeats;
if (slot.attendees && seatsPerTimeSlot) slotFull = slot.attendees >= seatsPerTimeSlot;
if (slot.attendees && bookingAttendees && seatsPerTimeSlot)
notEnoughSeats = slot.attendees + bookingAttendees > seatsPerTimeSlot;
return (
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
{/* Current there is no way to disable Next.js Links */}
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
{seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? (
<div
className={classNames(
"text-primary-500 dark:bg-darkgray-200 dark:text-darkgray-900 mb-2 block rounded-sm border bg-white py-2 font-medium opacity-25 dark:border-transparent ",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}>
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
{notEnoughSeats ? (
<p className="text-sm">{t("not_enough_seats")}</p>
) : slots ? (
<p className="text-sm">{t("booking_full")}</p>
) : null}
</div>
) : (
<Link
@@ -46,14 +46,16 @@ const BookingDescription: FC<Props> = (props) => {
const { profile, eventType, isBookingPage = false, children } = props;
const { date: bookingDate } = useRouterQuery("date");
const { t } = useLocale();
const { duration = eventType.length.toString(), setQuery: setDuration } = useRouterQuery("duration");
const { duration, setQuery: setDuration } = useRouterQuery("duration");
useEffect(() => {
if (
eventType.metadata?.multipleDuration &&
!eventType.metadata?.multipleDuration?.includes(Number(duration))
!duration ||
isNaN(Number(duration)) ||
(eventType.metadata?.multipleDuration &&
!eventType.metadata?.multipleDuration.includes(Number(duration)))
) {
setDuration(eventType.length.toString());
setDuration(eventType.length);
}
}, [duration, setDuration, eventType.length, eventType.metadata?.multipleDuration]);
@@ -81,20 +83,20 @@ const BookingDescription: FC<Props> = (props) => {
size="sm"
truncateAfter={3}
/>
<h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
<h2 className="mt-1 mb-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
{profile.name}
</h2>
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold text-gray-900">
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold leading-none text-gray-900">
{eventType.title}
</h1>
<div className="dark:text-darkgray-600 flex flex-col space-y-4 text-sm font-medium text-gray-600">
{eventType?.description && (
<div
className={classNames(
"flex",
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ",
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
)}>
<div className="scroll-bar scrollbar-track-w-20 max-h-[200px] max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
<div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
<EventTypeDescriptionSafeHTML eventType={eventType} />
</div>
</div>
@@ -133,7 +135,6 @@ const BookingDescription: FC<Props> = (props) => {
<li key={idx}>
<Badge
variant="gray"
size="lg"
className={classNames(
duration === dur.toString()
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
+30 -13
View File
@@ -46,7 +46,10 @@ function BookingListItem(booking: BookingItemProps) {
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const user = query.data;
const { t } = useLocale();
const {
t,
i18n: { language },
} = useLocale();
const utils = trpc.useContext();
const router = useRouter();
const [rejectionReason, setRejectionReason] = useState<string>("");
@@ -94,6 +97,13 @@ function BookingListItem(booking: BookingItemProps) {
mutation.mutate(body);
};
const getSeatReferenceUid = () => {
if (!booking.seatsReferences[0]) {
return undefined;
}
return booking.seatsReferences[0].referenceUid;
};
const pendingActions: ActionType[] = [
{
id: "reject",
@@ -135,7 +145,8 @@ function BookingListItem(booking: BookingItemProps) {
cancel all remaining bookings or just that booking instance. */
href: `/booking/${booking.uid}?cancel=true${
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}`,
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
`,
icon: FiX,
},
{
@@ -146,7 +157,9 @@ function BookingListItem(booking: BookingItemProps) {
id: "reschedule",
icon: FiClock,
label: t("reschedule_booking"),
href: `/reschedule/${booking.uid}`,
href: `/reschedule/${booking.uid}${
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
}`,
},
{
id: "reschedule_request",
@@ -179,14 +192,15 @@ function BookingListItem(booking: BookingItemProps) {
const RequestSentMessage = () => {
return (
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
<FiSend className="-mt-[1px] w-4 rotate-45" />
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
</div>
<Badge startIcon={FiSend} size="md" variant="gray" data-testid="request_reschedule_sent">
{t("reschedule_request_sent")}
</Badge>
);
};
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
const startTime = dayjs(booking.startTime)
.locale(language)
.format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
@@ -224,7 +238,7 @@ function BookingListItem(booking: BookingItemProps) {
const showRecordingsButtons =
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
const title = decodeURIComponent(booking.title);
const title = booking.title;
return (
<>
<RescheduleDialog
@@ -386,13 +400,13 @@ function BookingListItem(booking: BookingItemProps) {
/>
)}
{isCancelled && booking.rescheduled && (
<div className="mt-2 inline-block text-left text-sm md:hidden">
<div className="mt-2 inline-block md:hidden">
<RequestSentMessage />
</div>
)}
</div>
</td>
<td className="py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:pl-0">
<td className="flex w-full justify-end py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:pl-0">
{isUpcoming && !isCancelled ? (
<>
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
@@ -422,7 +436,10 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const user = query.data;
const { t } = useLocale();
const {
t,
i18n: { language },
} = useLocale();
const now = new Date();
const recurringCount = recurringDates.filter((date) => {
return (
@@ -452,7 +469,7 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
<p key={key} className={classNames(pastOrCancelled && "line-through")}>
{formatTime(aDate, user?.timeFormat, user?.timeZone)}
{" - "}
{dayjs(aDate).format("D MMMM YYYY")}
{dayjs(aDate).locale(language).format("D MMMM YYYY")}
</p>
);
})}>
+18 -9
View File
@@ -1,5 +1,5 @@
import { useRouter } from "next/router";
import { useState } from "react";
import { useCallback, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
@@ -23,18 +23,26 @@ type Props = {
setIsCancellationMode: (value: boolean) => void;
theme: string | null;
allRemainingBookings: boolean;
seatReferenceUid?: string;
};
export default function CancelBooking(props: Props) {
const [cancellationReason, setCancellationReason] = useState<string>("");
const { t } = useLocale();
const router = useRouter();
const { booking, allRemainingBookings } = props;
const { booking, allRemainingBookings, seatReferenceUid } = props;
const [loading, setLoading] = useState(false);
const telemetry = useTelemetry();
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
useTheme(props.theme);
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
if (node !== null) {
node.scrollIntoView({ behavior: "smooth" });
node.focus();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
{error && (
@@ -53,6 +61,7 @@ export default function CancelBooking(props: Props) {
<div className="mt-5 sm:mt-6">
<label className="text-bookingdark font-medium dark:text-white">{t("cancellation_reason")}</label>
<TextArea
ref={cancelBookingRef}
placeholder={t("cancellation_reason_placeholder")}
value={cancellationReason}
onChange={(e) => setCancellationReason(e.target.value)}
@@ -73,16 +82,16 @@ export default function CancelBooking(props: Props) {
onClick={async () => {
setLoading(true);
const payload = {
id: booking?.id,
cancellationReason: cancellationReason,
allRemainingBookings,
};
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
const res = await fetch("/api/cancel", {
body: JSON.stringify(payload),
body: JSON.stringify({
uid: booking?.uid,
cancellationReason: cancellationReason,
allRemainingBookings,
// @NOTE: very important this shouldn't cancel with number ID use uid instead
seatReferenceUid,
}),
headers: {
"Content-Type": "application/json",
},
+4 -1
View File
@@ -78,6 +78,7 @@ export const SlotPicker = ({
recurringEventCount,
users,
seatsPerTimeSlot,
bookingAttendees,
weekStart = 0,
ethSignature,
}: {
@@ -89,6 +90,7 @@ export const SlotPicker = ({
onTimeFormatChange: (is24Hour: boolean) => void;
timeZone?: string;
seatsPerTimeSlot?: number;
bookingAttendees?: number;
recurringEventCount?: number;
users: string[];
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
@@ -164,7 +166,7 @@ export const SlotPicker = ({
<DatePicker
isLoading={isLoading}
className={classNames(
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 lg:min-w-[455px]",
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]",
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
)}
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
@@ -192,6 +194,7 @@ export const SlotPicker = ({
eventTypeId={eventType.id}
eventTypeSlug={eventType.slug}
seatsPerTimeSlot={seatsPerTimeSlot}
bookingAttendees={bookingAttendees}
recurringCount={recurringEventCount}
ethSignature={ethSignature}
/>
@@ -17,7 +17,7 @@ export function TimezoneDropdown({
return (
<>
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white">
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</div>
@@ -23,8 +23,9 @@ import notEmpty from "@calcom/lib/notEmpty";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc";
import { HeadSeo } from "@calcom/ui";
import { FiCreditCard, FiRefreshCcw } from "@calcom/ui/components/icon";
import { FiCreditCard, FiUser, FiRefreshCcw } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
@@ -52,6 +53,7 @@ const dateQuerySchema = z.object({
rescheduleUid: z.string().optional().default(""),
date: z.string().optional().default(""),
timeZone: z.string().optional().default(""),
seatReferenceUid: z.string().optional(),
});
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
@@ -124,6 +126,15 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
: undefined,
];
const { data: bookingAttendees } = trpc.viewer.bookings.getBookingAttendees.useQuery(
{
seatReferenceUid: rescheduleUid,
},
{
enabled: !!(rescheduleUid && eventType.seatsPerTimeSlot),
}
);
return (
<Gates gates={gates} appData={rainbowAppData} dispatch={gateDispatcher}>
<HeadSeo
@@ -143,6 +154,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
nofollow: eventType.hidden,
noindex: eventType.hidden,
}}
isBrandingHidden={restProps.isBrandingHidden}
/>
<BookingPageTagManager eventType={eventType} />
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
@@ -168,10 +180,26 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
<div
className={classNames(
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
"min-w-full md:w-[230px] md:min-w-[230px]",
"min-w-full md:w-[280px] md:min-w-[280px]",
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
)}>
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
{rescheduleUid && eventType.seatsPerTimeSlot && bookingAttendees && (
<div
className={classNames(
"flex flex-nowrap items-center text-sm font-medium",
"dark:text-darkgray-600 text-gray-600",
"ltr:mr-[10px] rtl:ml-[10px]"
)}>
<FiUser
className={classNames(
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
"mt-[2px]"
)}
/>{" "}
{t("event_type_seats", { numberOfSeats: bookingAttendees })}
</div>
)}
{!rescheduleUid && eventType.recurringEvent && (
<div className="flex items-start text-sm font-medium">
<FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
@@ -247,6 +275,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
timeZone={timeZone}
users={userList}
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
bookingAttendees={bookingAttendees || undefined}
recurringEventCount={recurringEventCount}
ethSignature={gateState.rainbowToken}
/>
@@ -35,18 +35,19 @@ import classNames from "@calcom/lib/classNames";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { Button, Form, Tooltip } from "@calcom/ui";
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import createBooking from "@lib/mutations/bookings/create-booking";
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
import { parseDate, parseRecurringDates } from "@lib/parseDate";
import { parseRecurringDates, parseDate } from "@lib/parseDate";
import type { Gate, GateState } from "@components/Gates";
import Gates from "@components/Gates";
@@ -56,6 +57,8 @@ import type { BookPageProps } from "../../../pages/[user]/book";
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
/** These are like 40kb that not every user needs */
const BookingDescriptionPayment = dynamic(
() => import("@components/booking/BookingDescriptionPayment")
@@ -76,6 +79,7 @@ const BookingFields = ({
const { t } = useLocale();
const { watch, setValue } = useFormContext();
const locationResponse = watch("responses.location");
const currentView = rescheduleUid ? "reschedule" : "";
return (
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
@@ -85,12 +89,16 @@ const BookingFields = ({
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
let readOnly =
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
let noLabel = false;
let hidden = !!field.hidden;
const fieldViews = field.views;
if (fieldViews && !fieldViews.find((view) => view.id === currentView)) {
return null;
}
if (field.name === SystemField.Enum.rescheduleReason) {
if (!rescheduleUid) {
return null;
}
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
readOnly = false;
}
@@ -113,7 +121,10 @@ const BookingFields = ({
}
// We don't show `notes` field during reschedule
if (field.name === SystemField.Enum.notes && !!rescheduleUid) {
if (
(field.name === SystemField.Enum.notes || field.name === SystemField.Enum.guests) &&
!!rescheduleUid
) {
return null;
}
@@ -175,6 +186,22 @@ const BookingFields = ({
);
};
const routerQuerySchema = z
.object({
timeFormat: z.nativeEnum(TimeFormat),
rescheduleUid: z.string().optional(),
date: z
.string()
.optional()
.transform((date) => {
if (date === undefined) {
return null;
}
return date;
}),
})
.passthrough();
const BookingPage = ({
eventType,
booking,
@@ -226,12 +253,12 @@ const BookingPage = ({
const mutation = useMutation(createBooking, {
onSuccess: async (responseData) => {
const { uid, paymentUid } = responseData;
const { uid } = responseData;
if (paymentUid) {
if ("paymentUid" in responseData && !!responseData.paymentUid) {
return await router.push(
createPaymentLink({
paymentUid,
paymentUid: responseData.paymentUid,
date,
name: bookingForm.getValues("responses.name"),
email: bookingForm.getValues("responses.email"),
@@ -246,6 +273,7 @@ const BookingPage = ({
isSuccessBookingPage: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
},
});
@@ -268,11 +296,17 @@ const BookingPage = ({
},
});
const rescheduleUid = router.query.rescheduleUid as string;
const {
data: { timeFormat, rescheduleUid, date },
} = useTypedQuery(routerQuerySchema);
useTheme(profile.theme);
const date = asStringOrNull(router.query.date);
const querySchema = getBookingResponsesPartialSchema({
bookingFields: getBookingFieldsWithSystemFields(eventType),
eventType: {
bookingFields: getBookingFieldsWithSystemFields(eventType),
},
view: rescheduleUid ? "reschedule" : "booking",
});
const parsedQuery = querySchema.parse({
@@ -352,7 +386,8 @@ const BookingPage = ({
const bookingFormSchema = z
.object({
responses: getBookingResponsesSchema({
bookingFields: getBookingFieldsWithSystemFields(eventType),
eventType: { bookingFields: getBookingFieldsWithSystemFields(eventType) },
view: rescheduleUid ? "reschedule" : "booking",
}),
})
.passthrough();
@@ -370,6 +405,7 @@ const BookingPage = ({
// Calculate the booking date(s)
let recurringStrings: string[] = [],
recurringDates: Date[] = [];
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
[recurringStrings, recurringDates] = parseRecurringDates(
{
@@ -377,12 +413,13 @@ const BookingPage = ({
timeZone: timeZone(),
recurringEvent: eventType.recurringEvent,
recurringCount: parseInt(recurringEventCount.toString()),
selectedTimeFormat: timeFormat,
},
i18n
);
}
const bookEvent = (booking: BookingFormValues) => {
const bookEvent = (bookingValues: BookingFormValues) => {
telemetry.event(
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
{ isTeamBooking: document.URL.includes("team/") }
@@ -405,9 +442,9 @@ const BookingPage = ({
// Identify set of bookings to one intance of recurring event to support batch changes
const recurringEventId = uuidv4();
const recurringBookings = recurringDates.map((recurringDate) => ({
...booking,
start: dayjs(recurringDate).format(),
end: dayjs(recurringDate).add(duration, "minute").format(),
...bookingValues,
start: dayjs(recurringDate).utc().format(),
end: dayjs(recurringDate).utc().add(duration, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
recurringEventId,
@@ -425,20 +462,21 @@ const BookingPage = ({
recurringMutation.mutate(recurringBookings);
} else {
mutation.mutate({
...booking,
start: dayjs(date).tz(timeZone()).format(),
end: dayjs(date).tz(timeZone()).add(duration, "minute").format(),
...bookingValues,
start: dayjs(date).utc().format(),
end: dayjs(date).utc().add(duration, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
timeZone: timeZone(),
language: i18n.language,
rescheduleUid,
bookingUid: router.query.bookingUid as string,
bookingUid: (router.query.bookingUid as string) || booking?.uid,
user: router.query.user,
metadata,
hasHashedBookingLink,
hashedLink,
ethSignature: gateState.rainbowToken,
seatReferenceUid: router.query.seatReferenceUid as string,
});
}
};
@@ -481,7 +519,7 @@ const BookingPage = ({
)}>
<div
className={classNames(
"main overflow-hidden",
"main",
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border",
"dark:border-darkgray-300 rounded-md sm:border"
)}>
@@ -507,7 +545,7 @@ const BookingPage = ({
<div className="text-sm font-medium">
{isClientTimezoneAvailable &&
(rescheduleUid || !eventType.recurringEvent?.freq) &&
`${parseDate(date, i18n)}`}
`${parseDate(date, i18n, timeFormat)}`}
{isClientTimezoneAvailable &&
!rescheduleUid &&
eventType.recurringEvent?.freq &&
@@ -537,7 +575,7 @@ const BookingPage = ({
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{isClientTimezoneAvailable &&
typeof booking.startTime === "string" &&
parseDate(dayjs(booking.startTime), i18n)}
parseDate(dayjs(booking.startTime), i18n, timeFormat)}
</p>
</div>
)}
@@ -606,6 +644,7 @@ const BookingPage = ({
</div>
</div>
</main>
<Toaster position="bottom-right" />
</Gates>
);
};
@@ -615,17 +654,28 @@ export default BookingPage;
function ErrorMessage({ error }: { error: unknown }) {
const { t } = useLocale();
const { query: { rescheduleUid } = {} } = useRouter();
const router = useRouter();
return (
<div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
<FiAlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
</div>
<div className="ltr:ml-3 rtl:mr-3">
<p className="text-sm text-yellow-700">
<p className="text-sm text-blue-700">
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
{error instanceof HttpError || error instanceof Error ? t(error.message) : "Unknown error"}
{error instanceof HttpError || error instanceof Error ? (
<>
{t("can_you_try_again")}{" "}
<span className="cursor-pointer underline" onClick={() => router.back()}>
{t("go_back")}
</span>
.
</> /* t(error.message) */
) : (
"Unknown error"
)}
</p>
</div>
</div>
@@ -17,6 +17,7 @@ import {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Input } from "@calcom/ui";
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
import { FiMapPin } from "@calcom/ui/components/icon";
@@ -52,7 +53,7 @@ const LocationInput = (props: {
const { control } = useFormContext() as typeof locationFormMethods;
if (eventLocationType?.organizerInputType === "text") {
return (
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
<Input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
);
} else if (eventLocationType?.organizerInputType === "phone") {
return (
@@ -171,7 +172,6 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
id="locationInput"
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
required
className="block w-full rounded-sm border-gray-300 text-sm"
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
defaultValue={
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
@@ -288,16 +288,13 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
}}>
<QueryCell
query={locationsQuery}
success={({ data: locationOptions }) => {
if (!locationOptions.length) return null;
success={({ data }) => {
if (!data.length) return null;
const locationOptions = [...data];
if (booking) {
locationOptions.forEach((location) => {
if (location.label === "phone") {
location.options.filter((l) => l.value !== "phone");
} else if (location.label === "in person") {
location.options.filter((l) => l.value !== "attendeeInPerson");
}
});
locationOptions.map((location) =>
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
);
}
return (
<Controller
@@ -52,7 +52,8 @@ const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
type="text"
placeholder={placeHolder_}
{...register("customEventName", {
validate: (value) => validateCustomEventName(value, t("invalid_event_name_variables")),
validate: (value) =>
validateCustomEventName(value, t("invalid_event_name_variables"), event.bookingFields),
})}
className="mb-0"
/>
@@ -12,6 +12,7 @@ import DestinationCalendarSelector from "@calcom/features/calendars/DestinationC
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
@@ -36,11 +37,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const bookingFields: Prisma.JsonObject = {};
eventType.bookingFields.forEach(({ name }) => {
bookingFields[name] = name + " input";
});
const eventNameObject: EventNameObjectType = {
attendeeName: t("scheduler"),
eventType: eventType.title,
eventName: eventType.eventName,
host: eventType.users[0]?.name || "Nameless",
bookingFields: bookingFields,
t,
};
@@ -255,6 +264,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<SettingsToggle
data-testid="offer-seats-toggle"
title={t("offer_seats")}
description={t("offer_seats_description")}
checked={value}
@@ -308,7 +318,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<CustomEventTypeModal
close={closeEventNameTip}
setValue={setEventName}
defaultValue={eventType.eventName || ""}
defaultValue={formMethods.getValues("eventName") || eventType.eventName || ""}
placeHolder={eventNamePlaceholder}
event={eventNameObject}
/>
+175 -103
View File
@@ -1,9 +1,11 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import type { Key } from "react";
import React, { useEffect, useState } from "react";
import type { UseFormRegisterReturn } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { SingleValue } from "react-select";
import { classNames } from "@calcom/lib";
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
@@ -11,8 +13,17 @@ import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
import findDurationType from "@calcom/lib/findDurationType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { PeriodType } from "@calcom/prisma/client";
import type { BookingLimit } from "@calcom/types/Calendar";
import { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
import type { IntervalLimit } from "@calcom/types/Calendar";
import {
Button,
DateRangePicker,
Input,
InputField,
Label,
Select,
SettingsToggle,
TextField,
} from "@calcom/ui";
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
const MinimumBookingNoticeInput = React.forwardRef<
@@ -260,7 +271,34 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
formMethods.setValue("bookingLimits", {});
}
}}>
<BookingLimits />
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
</SettingsToggle>
)}
/>
<hr />
<Controller
name="durationLimits"
control={formMethods.control}
render={({ field: { value } }) => (
<SettingsToggle
title={t("limit_total_booking_duration")}
description={t("limit_total_booking_duration_description")}
checked={Object.keys(value ?? {}).length > 0}
onCheckedChange={(active) => {
if (active) {
formMethods.setValue("durationLimits", {
PER_DAY: 60,
});
} else {
formMethods.setValue("durationLimits", {});
}
}}>
<IntervalLimitsManager
propertyName="durationLimits"
defaultLimit={60}
step={15}
textFieldSuffix={t("minutes")}
/>
</SettingsToggle>
)}
/>
@@ -348,124 +386,158 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
);
};
const validationOrderKeys = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"];
type BookingLimitsKey = keyof BookingLimit;
const BookingLimits = () => {
type IntervalLimitsKey = keyof IntervalLimit;
const intervalOrderKeys = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"] as const;
const INTERVAL_LIMIT_OPTIONS = intervalOrderKeys.map((key) => ({
value: key as keyof IntervalLimit,
label: `Per ${key.split("_")[1].toLocaleLowerCase()}`,
}));
type IntervalLimitItemProps = {
key: Key;
limitKey: IntervalLimitsKey;
step: number;
value: number;
textFieldSuffix?: string;
selectOptions: { value: keyof IntervalLimit; label: string }[];
hasDeleteButton?: boolean;
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
onLimitChange: (intervalLimitsKey: IntervalLimitsKey, limit: number) => void;
onIntervalSelect: (interval: SingleValue<{ value: keyof IntervalLimit; label: string }>) => void;
};
const IntervalLimitItem = ({
limitKey,
step,
value,
textFieldSuffix,
selectOptions,
hasDeleteButton,
onDelete,
onLimitChange,
onIntervalSelect,
}: IntervalLimitItemProps) => {
return (
<div className="mb-2 flex items-center space-x-2 text-sm rtl:space-x-reverse" key={limitKey}>
<TextField
required
type="number"
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
placeholder={`${value}`}
min={step}
step={step}
defaultValue={value}
addOnSuffix={textFieldSuffix}
onChange={(e) => onLimitChange(limitKey, parseInt(e.target.value))}
/>
<Select
options={selectOptions}
isSearchable={false}
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
onChange={onIntervalSelect}
/>
{hasDeleteButton && (
<Button variant="icon" StartIcon={FiTrash} color="destructive" onClick={() => onDelete(limitKey)} />
)}
</div>
);
};
type IntervalLimitsManagerProps<K extends "durationLimits" | "bookingLimits"> = {
propertyName: K;
defaultLimit: number;
step: number;
textFieldSuffix?: string;
};
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
propertyName,
defaultLimit,
step,
textFieldSuffix,
}: IntervalLimitsManagerProps<K>) => {
const { watch, setValue, control } = useFormContext<FormValues>();
const watchBookingLimits = watch("bookingLimits");
const watchIntervalLimits = watch(propertyName);
const { t } = useLocale();
const [animateRef] = useAutoAnimate<HTMLUListElement>();
const BOOKING_LIMIT_OPTIONS: {
value: keyof BookingLimit;
label: string;
}[] = [
{
value: "PER_DAY",
label: "Per Day",
},
{
value: "PER_WEEK",
label: "Per Week",
},
{
value: "PER_MONTH",
label: "Per Month",
},
{
value: "PER_YEAR",
label: "Per Year",
},
];
return (
<Controller
name="bookingLimits"
name={propertyName}
control={control}
render={({ field: { value, onChange } }) => {
const currentBookingLimits = value;
const currentIntervalLimits = value;
const addLimit = () => {
if (!currentIntervalLimits || !watchIntervalLimits) return;
const currentKeys = Object.keys(watchIntervalLimits);
const [rest] = Object.values(INTERVAL_LIMIT_OPTIONS).filter(
(option) => !currentKeys.includes(option.value)
);
if (!rest || !currentKeys.length) return;
//currentDurationLimits is always defined so can be casted
// @ts-expect-error FIXME Fix these typings
setValue(propertyName, {
...watchIntervalLimits,
[rest.value]: defaultLimit,
});
};
return (
<ul ref={animateRef}>
{currentBookingLimits &&
watchBookingLimits &&
Object.entries(currentBookingLimits)
.sort(([limitkeyA], [limitKeyB]) => {
{currentIntervalLimits &&
watchIntervalLimits &&
Object.entries(currentIntervalLimits)
.sort(([limitKeyA], [limitKeyB]) => {
return (
validationOrderKeys.indexOf(limitkeyA as BookingLimitsKey) -
validationOrderKeys.indexOf(limitKeyB as BookingLimitsKey)
intervalOrderKeys.indexOf(limitKeyA as IntervalLimitsKey) -
intervalOrderKeys.indexOf(limitKeyB as IntervalLimitsKey)
);
})
.map(([key, bookingAmount]) => {
const bookingLimitKey = key as BookingLimitsKey;
.map(([key, value]) => {
const limitKey = key as IntervalLimitsKey;
return (
<div
className="mb-2 flex items-center space-x-2 text-sm rtl:space-x-reverse"
key={bookingLimitKey}>
<Input
id={`${bookingLimitKey}-limit`}
type="number"
className="mb-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
placeholder="1"
min={1}
defaultValue={bookingAmount}
onChange={(e) => {
const val = e.target.value;
setValue(`bookingLimits.${bookingLimitKey}`, parseInt(val));
}}
/>
<Select
options={BOOKING_LIMIT_OPTIONS.filter(
(option) => !Object.keys(currentBookingLimits).includes(option.value)
)}
isSearchable={false}
defaultValue={BOOKING_LIMIT_OPTIONS.find((option) => option.value === key)}
onChange={(val) => {
const current = currentBookingLimits;
const currentValue = watchBookingLimits[bookingLimitKey];
<IntervalLimitItem
key={key}
limitKey={limitKey}
step={step}
value={value}
textFieldSuffix={textFieldSuffix}
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
(option) => !Object.keys(currentIntervalLimits).includes(option.value)
)}
onLimitChange={(intervalLimitKey, val) =>
// @ts-expect-error FIXME Fix these typings
setValue(`${propertyName}.${intervalLimitKey}`, val)
}
onDelete={(intervalLimitKey) => {
const current = currentIntervalLimits;
delete current[intervalLimitKey];
onChange(current);
}}
onIntervalSelect={(interval) => {
const current = currentIntervalLimits;
const currentValue = watchIntervalLimits[limitKey];
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
delete current[bookingLimitKey];
const newData = {
...current,
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
[val?.value as BookingLimitsKey]: currentValue,
};
onChange(newData);
}}
/>
<Button
variant="icon"
StartIcon={FiTrash}
color="destructive"
onClick={() => {
const current = currentBookingLimits;
delete current[key as BookingLimitsKey];
onChange(current);
}}
/>
</div>
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
delete current[limitKey];
const newData = {
...current,
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
[interval?.value as IntervalLimitsKey]: currentValue,
};
onChange(newData);
}}
/>
);
})}
{currentBookingLimits && Object.keys(currentBookingLimits).length <= 3 && (
<Button
color="minimal"
StartIcon={FiPlus}
onClick={() => {
if (!currentBookingLimits || !watchBookingLimits) return;
const currentKeys = Object.keys(watchBookingLimits);
const rest = Object.values(BOOKING_LIMIT_OPTIONS).filter(
(option) => !currentKeys.includes(option.value)
);
if (!rest || !currentKeys) return;
//currentBookingLimits is always defined so can be casted
setValue("bookingLimits", {
...watchBookingLimits,
[rest[0].value]: 1,
});
}}>
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && (
<Button color="minimal" StartIcon={FiPlus} onClick={addLimit}>
{t("add_limit")}
</Button>
)}
@@ -192,16 +192,22 @@ export const EventSetupTab = (
if (!eventLocationType) {
return null;
}
// We dont want to translate the string link - it doesnt exist in common.json and it gets prefixed/suffixed with __ or //
const eventLabel =
eventLocationType.defaultValueVariable === "link"
? eventLocationType.label
: t(location[eventLocationType.defaultValueVariable] || eventLocationType.label);
location[eventLocationType.defaultValueVariable] || t(eventLocationType.label);
return (
<li
onClick={() => {
locationFormMethods.setValue("locationType", location.type);
locationFormMethods.unregister("locationLink");
locationFormMethods.unregister("locationAddress");
locationFormMethods.unregister("locationPhoneNumber");
setEditingLocationType(location.type);
openLocationModal(location.type);
}}
key={`${location.type}${index}`}
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer">
<div className="flex items-center justify-between">
<div className="flex items-center">
<img
+145 -111
View File
@@ -1,7 +1,7 @@
import { SchedulingType } from "@prisma/client";
import type { SchedulingType } from "@prisma/client";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef } from "react";
import type { ComponentProps } from "react";
import type { Control } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { Options } from "react-select";
@@ -35,65 +35,169 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
return 0;
};
const FixedHosts = ({
control,
const CheckedHostField = ({
labelText,
placeholder,
options = [],
isFixed,
value,
onChange,
...rest
}: {
control: Control<FormValues>;
labelText: string;
placeholder: string;
isFixed: boolean;
value: { isFixed: boolean; userId: number }[];
onChange?: (options: { isFixed: boolean; userId: number }[]) => void;
options?: Options<CheckedSelectOption>;
} & Partial<ComponentProps<typeof CheckedTeamSelect>>) => {
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
return (
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
<div>
<Label>{labelText}</Label>
<Controller
name="hostsFixed"
control={control}
render={({ field: { onChange, value } }) => {
return (
<CheckedTeamSelect
isDisabled={false}
onChange={(options) => {
onChange(
options.map((option) => ({
isFixed: true,
userId: parseInt(option.value, 10),
}))
);
}}
value={value
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={options}
placeholder={placeholder}
{...rest}
/>
);
<CheckedTeamSelect
isOptionDisabled={(option) => !!value.find((host) => host.userId.toString() === option.value)}
onChange={(options) => {
onChange &&
onChange(
options.map((option) => ({
isFixed,
userId: parseInt(option.value, 10),
}))
);
}}
value={(value || [])
.filter(({ isFixed: _isFixed }) => isFixed === _isFixed)
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={options}
placeholder={placeholder}
{...rest}
/>
</div>
</div>
);
};
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
const formMethods = useFormContext<FormValues>();
const RoundRobinHosts = ({
teamMembers,
value,
onChange,
}: {
value: { isFixed: boolean; userId: number }[];
onChange: (hosts: { isFixed: boolean; userId: number }[]) => void;
teamMembers: {
value: string;
label: string;
avatar: string;
email: string;
}[];
}) => {
const { t } = useLocale();
return (
<>
<CheckedHostField
options={teamMembers.sort(sortByLabel)}
isFixed={true}
onChange={(changeValue) => {
onChange([...value.filter(({ isFixed }) => !isFixed), ...changeValue]);
}}
value={value}
placeholder={t("add_fixed_hosts")}
labelText={t("fixed_hosts")}
/>
<CheckedHostField
options={teamMembers.sort(sortByLabel)}
onChange={(changeValue) => onChange([...value.filter(({ isFixed }) => isFixed), ...changeValue])}
value={value}
isFixed={false}
placeholder={t("add_attendees")}
labelText={t("round_robin_hosts")}
/>
</>
);
};
const Hosts = ({
teamMembers,
}: {
teamMembers: {
value: string;
label: string;
avatar: string;
email: string;
}[];
}) => {
const { t } = useLocale();
const {
control,
resetField,
getValues,
formState: { submitCount },
} = useFormContext<FormValues>();
const schedulingType = useWatch({
control: formMethods.control,
control,
name: "schedulingType",
});
const initialValue = useRef<{
hosts: FormValues["hosts"];
schedulingType: SchedulingType | null;
submitCount: number;
} | null>(null);
useEffect(() => {
// Handles init & out of date initial value after submission.
if (!initialValue.current || initialValue.current?.submitCount !== submitCount) {
initialValue.current = { hosts: getValues("hosts"), schedulingType, submitCount };
return;
}
resetField("hosts", {
defaultValue: initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
});
}, [schedulingType, resetField, getValues, submitCount]);
return (
<Controller<FormValues>
name="hosts"
render={({ field: { onChange, value } }) => {
const schedulingTypeRender = {
COLLECTIVE: (
<CheckedHostField
value={value}
onChange={onChange}
isFixed={true}
options={teamMembers.sort(sortByLabel)}
placeholder={t("add_attendees")}
labelText={t("team")}
/>
),
ROUND_ROBIN: (
<>
<RoundRobinHosts teamMembers={teamMembers} onChange={onChange} value={value} />
{/*<TextField
required
type="number"
label={t("minimum_round_robin_hosts_count")}
defaultValue={1}
{...formMethods.register("minimumHostCount")}
addOnSuffix={<>{t("hosts")}</>}
/>*/}
</>
),
};
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
}}
/>
);
};
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
const { t } = useLocale();
const schedulingTypeOptions: {
value: SchedulingType;
@@ -101,17 +205,16 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
// description: string;
}[] = [
{
value: SchedulingType.COLLECTIVE,
value: "COLLECTIVE",
label: t("collective"),
// description: t("collective_description"),
},
{
value: SchedulingType.ROUND_ROBIN,
value: "ROUND_ROBIN",
label: t("round_robin"),
// description: t("round_robin_description"),
},
];
const teamMembersOptions = teamMembers.map(mapUserToValue);
return (
<div>
@@ -119,9 +222,8 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
<div className="space-y-5">
<div className="flex flex-col">
<Label>{t("scheduling_type")}</Label>
<Controller
<Controller<FormValues>
name="schedulingType"
control={formMethods.control}
render={({ field: { value, onChange } }) => (
<Select
options={schedulingTypeOptions}
@@ -134,75 +236,7 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
)}
/>
</div>
{schedulingType === SchedulingType.COLLECTIVE && (
<FixedHosts
options={teamMembersOptions.sort(sortByLabel)}
placeholder={t("add_attendees")}
labelText={t("team")}
control={formMethods.control}
/>
)}
{schedulingType === SchedulingType.ROUND_ROBIN && (
<>
<FixedHosts
options={teamMembersOptions.sort(sortByLabel)}
isOptionDisabled={(option) =>
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
}
placeholder={t("add_fixed_hosts")}
labelText={t("fixed_hosts")}
control={formMethods.control}
/>
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
<div>
<Label>{t("round_robin_hosts")}</Label>
<Controller
name="hosts"
control={formMethods.control}
render={({ field: { onChange, value } }) => (
<CheckedTeamSelect
isDisabled={false}
onChange={(options) =>
onChange(
options.map((option) => ({
isFixed: false,
userId: parseInt(option.value, 10),
}))
)
}
value={value
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
teamMembers
.map(mapUserToValue)
.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={teamMembersOptions.sort(sortByLabel)}
isOptionDisabled={(option) =>
!!formMethods
.getValues("hostsFixed")
.find((host) => host.userId.toString() === option.value)
}
placeholder={t("add_attendees")}
/>
)}
/>
</div>
{/*<TextField
required
type="number"
label={t("minimum_round_robin_hosts_count")}
defaultValue={1}
{...formMethods.register("minimumHostCount")}
addOnSuffix={<>{t("hosts")}</>}
/>*/}
</div>
</>
)}
<Hosts teamMembers={teamMembersOptions} />
</div>
)}
</div>
@@ -220,6 +220,7 @@ function EventTypeSingleLayout({
<Tooltip content={t("preview")}>
<Button
color="secondary"
data-testid="preview-button"
target="_blank"
variant="icon"
href={permalink}
@@ -9,7 +9,7 @@ import type z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Alert, Input, Label, SettingsToggle } from "@calcom/ui";
import { Input, Label, SettingsToggle } from "@calcom/ui";
type RequiresConfirmationControllerProps = {
metadata: z.infer<typeof EventTypeMetaDataSchema>;
@@ -39,118 +39,116 @@ export default function RequiresConfirmationController({
return (
<div className="block items-start sm:flex">
<div className={!seatsEnabled ? "w-full" : ""}>
{seatsEnabled ? (
<Alert severity="warning" title="Seats option doesn't support confirmation requirement" />
) : (
<Controller
name="requiresConfirmation"
control={formMethods.control}
render={() => (
<SettingsToggle
title={t("requires_confirmation")}
description={t("requires_confirmation_description")}
checked={requiresConfirmation}
onCheckedChange={(val) => {
formMethods.setValue("requiresConfirmation", val);
onRequiresConfirmation(val);
}}>
<RadioGroup.Root
defaultValue={
requiresConfirmation
? requiresConfirmationSetup === undefined
? "always"
: "notice"
: undefined
<div className="w-full">
<Controller
name="requiresConfirmation"
control={formMethods.control}
render={() => (
<SettingsToggle
title={t("requires_confirmation")}
disabled={seatsEnabled}
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
description={t("requires_confirmation_description")}
checked={requiresConfirmation}
onCheckedChange={(val) => {
formMethods.setValue("requiresConfirmation", val);
onRequiresConfirmation(val);
}}>
<RadioGroup.Root
defaultValue={
requiresConfirmation
? requiresConfirmationSetup === undefined
? "always"
: "notice"
: undefined
}
onValueChange={(val) => {
if (val === "always") {
formMethods.setValue("requiresConfirmation", true);
onRequiresConfirmation(true);
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
setRequiresConfirmationSetup(undefined);
} else if (val === "notice") {
formMethods.setValue("requiresConfirmation", true);
onRequiresConfirmation(true);
formMethods.setValue(
"metadata.requiresConfirmationThreshold",
requiresConfirmationSetup || defaultRequiresConfirmationSetup
);
}
onValueChange={(val) => {
if (val === "always") {
formMethods.setValue("requiresConfirmation", true);
onRequiresConfirmation(true);
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
setRequiresConfirmationSetup(undefined);
} else if (val === "notice") {
formMethods.setValue("requiresConfirmation", true);
onRequiresConfirmation(true);
formMethods.setValue(
"metadata.requiresConfirmationThreshold",
requiresConfirmationSetup || defaultRequiresConfirmationSetup
);
}
}}>
<div className="-ml-2 flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
<div className="flex items-center">
<RadioGroup.Item
id="always"
value="always"
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="always" className="!m-0 flex items-center">
{t("always_requires_confirmation")}
</Label>
</div>
<div className="flex items-center">
<RadioGroup.Item
id="notice"
value="notice"
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="notice" className="!m-0 flex items-center">
<Trans
i18nKey="when_booked_with_less_than_notice"
defaults="When booked with less than <time></time> notice"
components={{
time: (
<div className="mx-2 flex">
<Input
type="number"
min={1}
onChange={(evt) => {
const val = Number(evt.target?.value);
setRequiresConfirmationSetup({
unit:
requiresConfirmationSetup?.unit ??
defaultRequiresConfirmationSetup.unit,
time: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
}}
className="!m-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
/>
<select
onChange={(evt) => {
const val = evt.target.value as UnitTypeLongPlural;
setRequiresConfirmationSetup({
time:
requiresConfirmationSetup?.time ??
defaultRequiresConfirmationSetup.time,
unit: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.unit", val);
}}
className="ml-2 block h-9 rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
defaultValue={
metadata?.requiresConfirmationThreshold?.unit ||
defaultRequiresConfirmationSetup.unit
}>
<option value="minutes">{t("minute_timeUnit")}</option>
<option value="hours">{t("hour_timeUnit")}</option>
</select>
</div>
),
}}
/>
</Label>
</div>
}}>
<div className="-ml-2 flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
<div className="flex items-center">
<RadioGroup.Item
id="always"
value="always"
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="always" className="!m-0 flex items-center">
{t("always_requires_confirmation")}
</Label>
</div>
</RadioGroup.Root>
</SettingsToggle>
)}
/>
)}
<div className="flex items-center">
<RadioGroup.Item
id="notice"
value="notice"
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="notice" className="!m-0 flex items-center">
<Trans
i18nKey="when_booked_with_less_than_notice"
defaults="When booked with less than <time></time> notice"
components={{
time: (
<div className="mx-2 flex">
<Input
type="number"
min={1}
onChange={(evt) => {
const val = Number(evt.target?.value);
setRequiresConfirmationSetup({
unit:
requiresConfirmationSetup?.unit ??
defaultRequiresConfirmationSetup.unit,
time: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
}}
className="!m-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
/>
<select
onChange={(evt) => {
const val = evt.target.value as UnitTypeLongPlural;
setRequiresConfirmationSetup({
time:
requiresConfirmationSetup?.time ??
defaultRequiresConfirmationSetup.time,
unit: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.unit", val);
}}
className="ml-2 block h-9 rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
defaultValue={
metadata?.requiresConfirmationThreshold?.unit ||
defaultRequiresConfirmationSetup.unit
}>
<option value="minutes">{t("minute_timeUnit")}</option>
<option value="hours">{t("hour_timeUnit")}</option>
</select>
</div>
),
}}
/>
</Label>
</div>
</div>
</RadioGroup.Root>
</SettingsToggle>
)}
/>
</div>
</div>
);
@@ -1,8 +1,11 @@
import { ArrowRightIcon } from "@heroicons/react/outline";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { trpc } from "@calcom/trpc/react";
@@ -22,23 +25,30 @@ const UserSettings = (props: IUserSettingsProps) => {
const { t } = useLocale();
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
const telemetry = useTelemetry();
const userSettingsSchema = z.object({
name: z
.string()
.min(1)
.max(FULL_NAME_LENGTH_MAX_LIMIT, {
message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
}),
});
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
} = useForm<z.infer<typeof userSettingsSchema>>({
defaultValues: {
name: user?.name || "",
},
reValidateMode: "onChange",
resolver: zodResolver(userSettingsSchema),
});
useEffect(() => {
telemetry.event(telemetryEventTypes.onboardingStarted);
}, [telemetry]);
const defaultOptions = { required: true, maxLength: 255 };
const utils = trpc.useContext();
const onSuccess = async () => {
await utils.viewer.me.invalidate();
@@ -67,7 +77,9 @@ const UserSettings = (props: IUserSettingsProps) => {
{t("full_name")}
</label>
<input
{...register("name", defaultOptions)}
{...register("name", {
required: true,
})}
id="name"
name="name"
type="text"
@@ -77,7 +89,7 @@ const UserSettings = (props: IUserSettingsProps) => {
/>
{errors.name && (
<p data-testid="required" className="py-2 text-xs text-red-500">
{t("required")}
{errors.name.message}
</p>
)}
</div>
@@ -1,7 +1,7 @@
import type { SyntheticEvent } from "react";
import { useState } from "react";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast } from "@calcom/ui";
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
@@ -2,7 +2,7 @@ import type { BaseSyntheticEvent } from "react";
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form } from "@calcom/ui";
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
@@ -2,7 +2,7 @@ import type { BaseSyntheticEvent } from "react";
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form, TextField } from "@calcom/ui";
+1 -1
View File
@@ -5,7 +5,7 @@ import React from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import * as z from "zod";
import { isPasswordValid } from "@calcom/lib/auth";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
+3 -3
View File
@@ -1,12 +1,12 @@
import Link from "next/link";
import type { TeamPageProps } from "pages/team/[slug]";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { Avatar } from "@calcom/ui";
type TeamType = TeamPageProps["team"];
type TeamType = NonNullable<TeamWithMembers>;
type MembersType = TeamType["members"];
type MemberType = MembersType[number];
@@ -57,7 +57,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
);
};
const Team = ({ team }: Omit<TeamPageProps, "trpcState">) => {
const Team = ({ team }: { team: TeamType }) => {
return (
<div>
<Members members={team.members} teamName={team.name} />
+4 -15
View File
@@ -3,8 +3,7 @@ import { components } from "react-select";
import type { EventLocationType } from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
import Select from "@components/ui/form/Select";
import { Select } from "@calcom/ui";
export type LocationOption = {
label: string;
@@ -17,21 +16,11 @@ export type SingleValueLocationOption = SingleValue<LocationOption>;
export type GroupOptionType = GroupBase<LocationOption>;
const OptionWithIcon = ({
icon,
isSelected,
label,
}: {
icon?: string;
isSelected?: boolean;
label: string;
}) => {
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
return (
<div className="flex items-center gap-3">
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5" />}
<span className={classNames("text-sm font-medium", isSelected ? "text-white" : "text-gray-900")}>
{label}
</span>
<span className={classNames("text-sm font-medium")}>{label}</span>
</div>
);
};
@@ -44,7 +33,7 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
components={{
Option: (props) => (
<components.Option {...props}>
<OptionWithIcon icon={props.data.icon} label={props.data.label} isSelected={props.isSelected} />
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
</components.Option>
),
SingleValue: (props) => (
-45
View File
@@ -1,45 +0,0 @@
import { IdentityProvider } from "@prisma/client";
import { compare, hash } from "bcryptjs";
import type { Session } from "next-auth";
import type { GetSessionParams } from "next-auth/react";
import { getSession as getSessionInner } from "next-auth/react";
/** @deprecated use the one from `@calcom/lib/auth` */
export async function hashPassword(password: string) {
const hashedPassword = await hash(password, 12);
return hashedPassword;
}
/** @deprecated use the one from `@calcom/lib/auth` */
export async function verifyPassword(password: string, hashedPassword: string) {
const isValid = await compare(password, hashedPassword);
return isValid;
}
/** @deprecated use the one from `@calcom/lib/auth` */
export async function getSession(options: GetSessionParams): Promise<Session | null> {
const session = await getSessionInner(options);
// that these are equal are ensured in `[...nextauth]`'s callback
return session as Session | null;
}
/** @deprecated use the one from `@calcom/lib/auth` */
export enum ErrorCode {
UserNotFound = "user-not-found",
IncorrectPassword = "incorrect-password",
UserMissingPassword = "missing-password",
TwoFactorDisabled = "two-factor-disabled",
TwoFactorAlreadyEnabled = "two-factor-already-enabled",
TwoFactorSetupRequired = "two-factor-setup-required",
SecondFactorRequired = "second-factor-required",
IncorrectTwoFactorCode = "incorrect-two-factor-code",
InternalServerError = "internal-server-error",
NewPasswordMatchesOld = "new-password-matches-old",
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
RateLimitExceeded = "rate-limit-exceeded",
InvalidPassword = "invalid-password",
}
/** @deprecated use the one from `@calcom/lib/auth` */
export const identityProviderNameMap: { [key in IdentityProvider]: string } = {
[IdentityProvider.CAL]: "Cal",
[IdentityProvider.GOOGLE]: "Google",
[IdentityProvider.SAML]: "SAML",
};
+9 -5
View File
@@ -1,10 +1,10 @@
import parser from "accept-language-parser";
import type { IncomingMessage } from "http";
import type { GetServerSidePropsContext } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { Maybe } from "@calcom/trpc/server";
import { getSession } from "@lib/auth";
import { i18n } from "../../../next-i18next.config";
export function getLocaleFromHeaders(req: IncomingMessage): string {
@@ -15,9 +15,13 @@ export function getLocaleFromHeaders(req: IncomingMessage): string {
return preferredLocale ?? i18n.defaultLocale;
}
export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage): Promise<string> => {
const prisma = (await import("@calcom/prisma")).default;
const session = await getSession({ req });
export const getOrSetUserLocaleFromHeaders = async (
req: GetServerSidePropsContext["req"],
res: GetServerSidePropsContext["res"]
): Promise<string> => {
const { default: prisma } = await import("@calcom/prisma");
const session = await getServerSession({ req, res });
const preferredLocale = getLocaleFromHeaders(req);
if (session?.user?.id) {
+13 -1
View File
@@ -55,6 +55,8 @@ async function getBooking(
uid,
},
select: {
id: true,
uid: true,
startTime: true,
description: true,
customInputs: true,
@@ -65,6 +67,12 @@ async function getBooking(
select: {
email: true,
name: true,
bookingSeat: true,
},
},
user: {
select: {
id: true,
},
},
},
@@ -104,7 +112,11 @@ export const getBookingWithResponses = <
return {
...booking,
responses: getBookingResponsesPartialSchema({
bookingFields: eventType.bookingFields,
eventType: {
bookingFields: eventType.bookingFields,
},
// An existing booking can have data from any number of views, so the schema should consider ALL_VIEWS
view: "ALL_VIEWS",
}).parse(booking.responses || getResponsesFromOldBooking(booking)),
};
};
+8 -5
View File
@@ -3,21 +3,22 @@ import { RRule } from "rrule";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import type { TimeFormat } from "@calcom/lib/timeFormat";
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { parseZone } from "./parseZone";
const processDate = (date: string | null | Dayjs, i18n: I18n) => {
const processDate = (date: string | null | Dayjs, i18n: I18n, selectedTimeFormat?: TimeFormat) => {
const parsedZone = parseZone(date);
if (!parsedZone?.isValid()) return "Invalid date";
const formattedTime = parsedZone?.format(detectBrowserTimeFormat);
const formattedTime = parsedZone?.format(selectedTimeFormat || detectBrowserTimeFormat);
return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" });
};
export const parseDate = (date: string | null | Dayjs, i18n: I18n) => {
export const parseDate = (date: string | null | Dayjs, i18n: I18n, selectedTimeFormat?: TimeFormat) => {
if (!date) return ["No date"];
return processDate(date, i18n);
return processDate(date, i18n, selectedTimeFormat);
};
export const parseRecurringDates = (
@@ -26,11 +27,13 @@ export const parseRecurringDates = (
timeZone,
recurringEvent,
recurringCount,
selectedTimeFormat,
}: {
startDate: string | null | Dayjs;
timeZone?: string;
recurringEvent: RecurringEvent | null;
recurringCount: number;
selectedTimeFormat?: TimeFormat;
},
i18n: I18n
): [string[], Date[]] => {
@@ -50,7 +53,7 @@ export const parseRecurringDates = (
});
const dateStrings = times.map((t) => {
// finally; show in local timeZone again
return processDate(t.tz(timeZone), i18n);
return processDate(t.tz(timeZone), i18n, selectedTimeFormat);
});
return [dateStrings, times.map((t) => t.toDate())];
+3 -9
View File
@@ -1,9 +1,3 @@
import type { Attendee, Booking } from "@prisma/client";
import type { AppsStatus } from "@calcom/types/Calendar";
export type BookingResponse = Booking & {
paymentUid?: string;
attendees: Attendee[];
appsStatus?: AppsStatus[];
};
export type BookingResponse = Awaited<
ReturnType<typeof import("@calcom/features/bookings/lib/handleNewBooking").default>
>;
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.6.7",
"version": "2.7.3",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -19,18 +19,18 @@
"check-changed-files": "ts-node scripts/ts-check-changed-files.ts"
},
"engines": {
"node": "16.x || 18.x",
"yarn": ">=1.19.0 < 2.0.0"
"node": "18",
"yarn": "3.4.1"
},
"dependencies": {
"@boxyhq/saml-jackson": "1.7.1",
"@boxyhq/saml-jackson": "1.8.2",
"@calcom/app-store": "*",
"@calcom/app-store-cli": "*",
"@calcom/core": "*",
"@calcom/dayjs": "*",
"@calcom/embed-core": "*",
"@calcom/embed-react": "*",
"@calcom/embed-snippet": "*",
"@calcom/embed-core": "workspace:*",
"@calcom/embed-react": "workspace:*",
"@calcom/embed-snippet": "workspace:*",
"@calcom/features": "*",
"@calcom/lib": "*",
"@calcom/prisma": "*",
@@ -69,6 +69,7 @@
"bcryptjs": "^2.4.3",
"classnames": "^2.3.1",
"dotenv-cli": "^6.0.0",
"entities": "^4.4.0",
"eslint-config-next": "^13.2.1",
"googleapis": "^84.0.0",
"gray-matter": "^4.0.3",
@@ -86,7 +87,7 @@
"micro": "^10.0.1",
"mime-types": "^2.1.35",
"next": "^13.2.1",
"next-auth": "^4.18.8",
"next-auth": "^4.20.1",
"next-axiom": "^0.16.0",
"next-collect": "^0.2.1",
"next-i18next": "^11.3.0",
@@ -110,8 +111,8 @@
"react-multi-email": "^0.5.3",
"react-phone-number-input": "^3.2.7",
"react-schemaorg": "^2.0.0",
"react-select": "^5.4.0",
"react-timezone-select": "^1.3.2",
"react-select": "^5.7.0",
"react-timezone-select": "^1.4.0",
"react-use-intercom": "1.5.1",
"react-virtualized-auto-sizer": "^1.0.6",
"react-window": "^1.8.7",
@@ -133,7 +134,7 @@
"@calcom/config": "*",
"@calcom/types": "*",
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
"@playwright/test": "^1.25.0",
"@playwright/test": "^1.31.2",
"@testing-library/react": "^13.3.0",
"@types/accept-language-parser": "1.5.2",
"@types/async": "^3.2.15",
@@ -161,7 +162,6 @@
"copy-webpack-plugin": "^11.0.0",
"detect-port": "^1.3.0",
"env-cmd": "^10.1.0",
"eslint": "^8.34.0",
"mockdate": "^3.0.5",
"module-alias": "^2.2.2",
"msw": "^0.42.3",
+30 -23
View File
@@ -15,6 +15,7 @@ import {
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import CustomBranding from "@calcom/lib/CustomBranding";
import { WEBAPP_URL } from "@calcom/lib/constants";
import defaultEvents, {
getDynamicEventDescription,
getGroupName,
@@ -76,7 +77,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
size="sm"
items={props.users.map((user) => ({
alt: user.name || "",
image: user.avatar || "",
image: user.avatar,
}))}
/>
</div>
@@ -177,26 +178,28 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
className="dark:bg-darkgray-100 dark:hover:bg-darkgray-200 dark:border-darkgray-300 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50">
<FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<Link
prefetch={false}
href={{
pathname: `/${user.username}/${type.slug}`,
query,
}}
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
className="block w-full p-5"
data-testid="event-type-link">
<div className="flex flex-wrap items-center">
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
{type.title}
</h2>
</div>
<EventTypeDescription eventType={type} />
</Link>
<div className="block w-full p-5">
<Link
prefetch={false}
href={{
pathname: `/${user.username}/${type.slug}`,
query,
}}
passHref
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
data-testid="event-type-link">
<div className="flex flex-wrap items-center">
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
{type.title}
</h2>
</div>
<EventTypeDescription eventType={type} />
</Link>
</div>
</div>
))
)}
@@ -259,7 +262,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const usernameList = getUsernameList(context.query.user as string);
const dataFetchStart = Date.now();
const users = await prisma.user.findMany({
const usersWithoutAvatar = await prisma.user.findMany({
where: {
username: {
in: usernameList,
@@ -273,7 +276,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
bio: true,
brandColor: true,
darkBrandColor: true,
avatar: true,
theme: true,
away: true,
verified: true,
@@ -281,6 +283,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
});
const users = usersWithoutAvatar.map((user) => ({
...user,
avatar: `${WEBAPP_URL}/${user.username}/avatar.png`,
}));
if (!users.length) {
return {
notFound: true,
+92 -34
View File
@@ -1,8 +1,10 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import type { LocationObject } from "@calcom/app-store/locations";
import { privacyFilteredLocations } from "@calcom/app-store/locations";
import { getAppFromSlug } from "@calcom/app-store/utils";
import dayjs from "@calcom/dayjs";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { parseRecurringEvent } from "@calcom/lib";
import {
@@ -12,15 +14,13 @@ import {
getUsernameList,
} from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { bookEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma";
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
import {
customInputSchema,
EventTypeMetaDataSchema,
userMetadata as userMetadataSchema,
} from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import type { GetBookingType } from "@lib/getBooking";
import getBooking from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -70,11 +70,26 @@ export default function Book(props: BookPageProps) {
Book.isThemeSupported = true;
const querySchema = z.object({
bookingUid: z.string().optional(),
count: z.coerce.number().optional(),
embed: z.string().optional(),
rescheduleUid: z.string().optional(),
slug: z.string().optional(),
/** This is the event "type" ID */
type: z.coerce.number().optional(),
user: z.string(),
seatReferenceUid: z.string().optional(),
date: z.string().optional(),
duration: z.coerce.number().optional(),
});
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const usernameList = getUsernameList(asStringOrThrow(context.query.user as string));
const eventTypeSlug = context.query.slug as string;
const recurringEventCountQuery = asStringOrNull(context.query.count);
const query = querySchema.parse(context.query);
const usernameList = getUsernameList(query.user);
const eventTypeSlug = query.slug;
const recurringEventCountQuery = query.count;
const users = await prisma.user.findMany({
where: {
username: {
@@ -99,24 +114,23 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!users.length) return { notFound: true };
const [user] = users;
const isDynamicGroupBooking = users.length > 1;
const isDynamicGroupBooking = users.length > 1 && !!eventTypeSlug;
// Dynamic Group link doesn't need a type but it must have a slug
if ((!isDynamicGroupBooking && !context.query.type) || (isDynamicGroupBooking && !eventTypeSlug)) {
if ((!isDynamicGroupBooking && !query.type) || (users.length > 1 && !eventTypeSlug)) {
return { notFound: true };
}
const eventTypeRaw =
usernameList.length > 1
? getDefaultEvent(eventTypeSlug)
: await prisma.eventType.findUnique({
where: {
id: parseInt(asStringOrThrow(context.query.type)),
},
select: {
...bookEventTypeSelect,
},
});
const eventTypeRaw = isDynamicGroupBooking
? getDefaultEvent(eventTypeSlug)
: await prisma.eventType.findUnique({
where: {
id: query.type,
},
select: {
...bookEventTypeSelect,
},
});
if (!eventTypeRaw) return { notFound: true };
const eventType = {
@@ -178,22 +192,68 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
};
})[0];
// If rescheduleUid and event has seats lets convert Uid to bookingUid
let rescheduleUid = query.rescheduleUid;
const rescheduleEventTypeHasSeats = query.rescheduleUid && eventTypeRaw.seatsPerTimeSlot;
let attendeeEmail: string;
let bookingUidWithSeats: string | null = null;
if (rescheduleEventTypeHasSeats) {
const bookingSeat = await prisma.bookingSeat.findFirst({
where: {
referenceUid: query.rescheduleUid,
},
select: {
id: true,
attendee: true,
booking: {
select: {
uid: true,
},
},
},
});
if (bookingSeat) {
rescheduleUid = bookingSeat.booking.uid;
attendeeEmail = bookingSeat.attendee.email;
}
} else if (eventTypeRaw.seatsPerTimeSlot && query.duration && query.date) {
// If it's not reschedule but event Type has seats we should obtain
// the bookingUid regardless and use it to get the booking
const currentSeats = await prisma.booking.findFirst({
where: {
eventTypeId: eventTypeRaw.id,
startTime: dayjs(query.date).toISOString(),
endTime: dayjs(query.date).add(query.duration, "minutes").toISOString(),
},
select: {
uid: true,
},
});
if (currentSeats && currentSeats) {
bookingUidWithSeats = currentSeats.uid;
}
}
let booking: GetBookingType | null = null;
if (context.query.rescheduleUid || context.query.bookingUid) {
if (rescheduleUid || query.bookingUid || bookingUidWithSeats) {
booking = await getBooking(
prisma,
context.query.rescheduleUid
? (context.query.rescheduleUid as string)
: (context.query.bookingUid as string),
rescheduleUid || query.bookingUid || bookingUidWithSeats || "",
eventTypeObject.bookingFields
);
}
const dynamicNames = isDynamicGroupBooking
? users.map((user) => {
return user.name || "";
})
: [];
if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) {
const currentAttendee = booking?.attendees.find((attendee) => {
return attendee.email === attendeeEmail;
});
if (currentAttendee) {
booking.attendees = [currentAttendee] || [];
}
}
const dynamicNames = isDynamicGroupBooking ? users.map((user) => user.name || "") : [];
const profile = isDynamicGroupBooking
? {
@@ -203,9 +263,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
theme: null,
brandColor: "",
darkBrandColor: "",
allowDynamicBooking: !users.some((user) => {
return !user.allowDynamicBooking;
}),
allowDynamicBooking: !users.some((user) => !user.allowDynamicBooking),
eventName: getDynamicEventName(dynamicNames, eventTypeSlug),
}
: {
@@ -222,8 +280,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const recurringEventCount =
(eventType.recurringEvent?.count &&
recurringEventCountQuery &&
(parseInt(recurringEventCountQuery) <= eventType.recurringEvent.count
? parseInt(recurringEventCountQuery)
(recurringEventCountQuery <= eventType.recurringEvent.count
? recurringEventCountQuery
: eventType.recurringEvent.count)) ||
null;
@@ -238,7 +296,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
isDynamicGroupBooking,
hasHashedBookingLink: false,
hashedLink: null,
isEmbed: typeof context.query.embed === "string",
isEmbed: !!query.embed,
},
};
}
+2 -589
View File
@@ -1,592 +1,5 @@
import type { UserPermissionRole } from "@prisma/client";
import { IdentityProvider } from "@prisma/client";
import { readFileSync } from "fs";
import Handlebars from "handlebars";
import { SignJWT } from "jose";
import type { Session } from "next-auth";
import NextAuth from "next-auth";
import { encode } from "next-auth/jwt";
import type { Provider } from "next-auth/providers";
import CredentialsProvider from "next-auth/providers/credentials";
import EmailProvider from "next-auth/providers/email";
import GoogleProvider from "next-auth/providers/google";
import type { TransportOptions } from "nodemailer";
import nodemailer from "nodemailer";
import { authenticator } from "otplib";
import path from "path";
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies";
import { randomString } from "@calcom/lib/random";
import rateLimit from "@calcom/lib/rateLimit";
import { serverConfig } from "@calcom/lib/serverConfig";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options";
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
const transporter = nodemailer.createTransport<TransportOptions>({
...(serverConfig.transport as TransportOptions),
} as TransportOptions);
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
const signJwt = async (payload: { email: string }) => {
const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY);
return new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.setSubject(payload.email)
.setIssuedAt()
.setIssuer(WEBSITE_URL)
.setAudience(`${WEBSITE_URL}/auth/login`)
.setExpirationTime("2m")
.sign(secret);
};
const loginWithTotp = async (user: { email: string }) =>
`/auth/login?totp=${await signJwt({ email: user.email })}`;
const providers: Provider[] = [
CredentialsProvider({
id: "credentials",
name: "Cal.com",
type: "credentials",
credentials: {
email: { label: "Email Address", type: "email", placeholder: "john.doe@example.com" },
password: { label: "Password", type: "password", placeholder: "Your super secure password" },
totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" },
},
async authorize(credentials) {
if (!credentials) {
console.error(`For some reason credentials are missing`);
throw new Error(ErrorCode.InternalServerError);
}
const user = await prisma.user.findUnique({
where: {
email: credentials.email.toLowerCase(),
},
select: {
role: true,
id: true,
username: true,
name: true,
email: true,
metadata: true,
identityProvider: true,
password: true,
twoFactorEnabled: true,
twoFactorSecret: true,
teams: {
include: {
team: true,
},
},
},
});
// Don't leak information about it being username or password that is invalid
if (!user) {
throw new Error(ErrorCode.IncorrectUsernamePassword);
}
if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
}
if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
throw new Error(ErrorCode.IncorrectUsernamePassword);
}
if (user.password) {
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
if (!isCorrectPassword) {
throw new Error(ErrorCode.IncorrectUsernamePassword);
}
}
if (user.twoFactorEnabled) {
if (!credentials.totpCode) {
throw new Error(ErrorCode.SecondFactorRequired);
}
if (!user.twoFactorSecret) {
console.error(`Two factor is enabled for user ${user.id} but they have no secret`);
throw new Error(ErrorCode.InternalServerError);
}
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error(`"Missing encryption key; cannot proceed with two factor login."`);
throw new Error(ErrorCode.InternalServerError);
}
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
if (secret.length !== 32) {
console.error(
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
);
throw new Error(ErrorCode.InternalServerError);
}
const isValidToken = authenticator.check(credentials.totpCode, secret);
if (!isValidToken) {
throw new Error(ErrorCode.IncorrectTwoFactorCode);
}
}
const limiter = rateLimit({
intervalInMs: 60 * 1000, // 1 minute
});
await limiter.check(10, user.email); // 10 requests per minute
// Check if the user you are logging into has any active teams
const hasActiveTeams =
user.teams.filter((m: { team: { metadata: unknown } }) => {
if (!IS_TEAM_BILLING_ENABLED) return true;
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return true;
return false;
}).length > 0;
// authentication success- but does it meet the minimum password requirements?
if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) {
return {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
role: "INACTIVE_ADMIN",
belongsToActiveTeam: hasActiveTeams,
};
}
return {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
role: user.role,
belongsToActiveTeam: hasActiveTeams,
};
},
}),
ImpersonationProvider,
];
if (IS_GOOGLE_LOGIN_ENABLED) {
providers.push(
GoogleProvider({
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
allowDangerousEmailAccountLinking: true,
})
);
}
if (isSAMLLoginEnabled) {
providers.push({
id: "saml",
name: "BoxyHQ",
type: "oauth",
version: "2.0",
checks: ["pkce", "state"],
authorization: {
url: `${WEBAPP_URL}/api/auth/saml/authorize`,
params: {
scope: "",
response_type: "code",
provider: "saml",
},
},
token: {
url: `${WEBAPP_URL}/api/auth/saml/token`,
params: { grant_type: "authorization_code" },
},
userinfo: `${WEBAPP_URL}/api/auth/saml/userinfo`,
profile: (profile) => {
return {
id: profile.id || "",
firstName: profile.firstName || "",
lastName: profile.lastName || "",
email: profile.email || "",
name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
email_verified: true,
};
},
options: {
clientId: "dummy",
clientSecret: "dummy",
},
allowDangerousEmailAccountLinking: true,
});
}
if (true) {
const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates");
providers.push(
EmailProvider({
type: "email",
maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only
// Here we setup the sendVerificationRequest that calls the email template with the identifier (email) and token to verify.
sendVerificationRequest: ({ identifier, url }) => {
const originalUrl = new URL(url);
const webappUrl = new URL(WEBAPP_URL);
if (originalUrl.origin !== webappUrl.origin) {
url = url.replace(originalUrl.origin, webappUrl.origin);
}
const emailFile = readFileSync(path.join(emailsDir, "confirm-email.html"), {
encoding: "utf8",
});
const emailTemplate = Handlebars.compile(emailFile);
transporter.sendMail({
from: `${process.env.EMAIL_FROM}` || APP_NAME,
to: identifier,
subject: "Your sign-in link for " + APP_NAME,
html: emailTemplate({
base_url: WEBAPP_URL,
signin_url: url,
email: identifier,
}),
});
},
})
);
}
function isNumber(n: string) {
return !isNaN(parseFloat(n)) && !isNaN(+n);
}
const calcomAdapter = CalComAdapter(prisma);
export default NextAuth({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
adapter: calcomAdapter,
session: {
strategy: "jwt",
},
jwt: {
// decorate the native JWT encode function
// Impl. detail: We don't pass through as this function is called with encode/decode functions.
encode: async ({ token, maxAge, secret }) => {
if (token?.sub && isNumber(token.sub)) {
const user = await prisma.user.findFirst({
where: { id: Number(token.sub) },
select: { metadata: true },
});
// if no user is found, we still don't want to crash here.
if (user) {
const metadata = userMetadata.parse(user.metadata);
if (metadata?.sessionTimeout) {
maxAge = metadata.sessionTimeout / 60;
}
}
}
return encode({ secret, token, maxAge });
},
},
cookies: defaultCookies(WEBAPP_URL?.startsWith("https://")),
pages: {
signIn: "/auth/login",
signOut: "/auth/logout",
error: "/auth/error", // Error code passed in query string as ?error=
verifyRequest: "/auth/verify",
// newUser: "/auth/new", // New users will be directed here on first sign in (leave the property out if not of interest)
},
providers,
callbacks: {
async jwt({ token, user, account }) {
const autoMergeIdentities = async () => {
const existingUser = await prisma.user.findFirst({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
where: { email: token.email! },
select: {
id: true,
username: true,
name: true,
email: true,
role: true,
},
});
if (!existingUser) {
return token;
}
return {
...existingUser,
...token,
};
};
if (!user) {
return await autoMergeIdentities();
}
if (account && account.type === "credentials") {
return {
...token,
id: user.id,
name: user.name,
username: user.username,
email: user.email,
role: user.role,
impersonatedByUID: user?.impersonatedByUID,
belongsToActiveTeam: user?.belongsToActiveTeam,
};
}
// The arguments above are from the provider so we need to look up the
// user based on those values in order to construct a JWT.
if (account && account.type === "oauth" && account.provider && account.providerAccountId) {
let idP: IdentityProvider = IdentityProvider.GOOGLE;
if (account.provider === "saml") {
idP = IdentityProvider.SAML;
}
const existingUser = await prisma.user.findFirst({
where: {
AND: [
{
identityProvider: idP,
},
{
identityProviderId: account.providerAccountId as string,
},
],
},
});
if (!existingUser) {
return await autoMergeIdentities();
}
return {
...token,
id: existingUser.id,
name: existingUser.name,
username: existingUser.username,
email: existingUser.email,
role: existingUser.role,
impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
};
}
return token;
},
async session({ session, token }) {
const hasValidLicense = await checkLicense(prisma);
const calendsoSession: Session = {
...session,
hasValidLicense,
user: {
...session.user,
id: token.id as number,
name: token.name,
username: token.username as string,
role: token.role as UserPermissionRole,
impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
},
};
return calendsoSession;
},
async signIn(params) {
const { user, account, profile } = params;
if (account?.provider === "email") {
return true;
}
// In this case we've already verified the credentials in the authorize
// callback so we can sign the user in.
if (account?.type === "credentials") {
return true;
}
if (account?.type !== "oauth") {
return false;
}
if (!user.email) {
return false;
}
if (!user.name) {
return false;
}
if (account?.provider) {
let idP: IdentityProvider = IdentityProvider.GOOGLE;
if (account.provider === "saml") {
idP = IdentityProvider.SAML;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error TODO validate email_verified key on profile
user.email_verified = user.email_verified || !!user.emailVerified || profile.email_verified;
if (!user.email_verified) {
return "/auth/error?error=unverified-email";
}
// Only google oauth on this path
const provider = account.provider.toUpperCase() as IdentityProvider;
const existingUser = await prisma.user.findFirst({
include: {
accounts: {
where: {
provider: account.provider,
},
},
},
where: {
identityProvider: provider,
identityProviderId: account.providerAccountId,
},
});
if (existingUser) {
// In this case there's an existing user and their email address
// hasn't changed since they last logged in.
if (existingUser.email === user.email) {
try {
// If old user without Account entry we link their google account
if (existingUser.accounts.length === 0) {
const linkAccountWithUserData = { ...account, userId: existingUser.id };
await calcomAdapter.linkAccount(linkAccountWithUserData);
}
} catch (error) {
if (error instanceof Error) {
console.error("Error while linking account of already existing user");
}
}
if (existingUser.twoFactorEnabled) {
return loginWithTotp(existingUser);
} else {
return true;
}
}
// If the email address doesn't match, check if an account already exists
// with the new email address. If it does, for now we return an error. If
// not, update the email of their account and log them in.
const userWithNewEmail = await prisma.user.findFirst({
where: { email: user.email },
});
if (!userWithNewEmail) {
await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } });
if (existingUser.twoFactorEnabled) {
return loginWithTotp(existingUser);
} else {
return true;
}
} else {
return "/auth/error?error=new-email-conflict";
}
}
// If there's no existing user for this identity provider and id, create
// a new account. If an account already exists with the incoming email
// address return an error for now.
const existingUserWithEmail = await prisma.user.findFirst({
where: { email: user.email },
});
if (existingUserWithEmail) {
// if self-hosted then we can allow auto-merge of identity providers if email is verified
if (!hostedCal && existingUserWithEmail.emailVerified) {
if (existingUserWithEmail.twoFactorEnabled) {
return loginWithTotp(existingUserWithEmail);
} else {
return true;
}
}
// check if user was invited
if (
!existingUserWithEmail.password &&
!existingUserWithEmail.emailVerified &&
!existingUserWithEmail.username
) {
await prisma.user.update({
where: { email: user.email },
data: {
// Slugify the incoming name and append a few random characters to
// prevent conflicts for users with the same name.
username: usernameSlug(user.name),
emailVerified: new Date(Date.now()),
name: user.name,
identityProvider: idP,
identityProviderId: String(user.id),
},
});
if (existingUserWithEmail.twoFactorEnabled) {
return loginWithTotp(existingUserWithEmail);
} else {
return true;
}
}
// User signs up with email/password and then tries to login with Google/SAML using the same email
if (
existingUserWithEmail.identityProvider === IdentityProvider.CAL &&
(idP === IdentityProvider.GOOGLE || idP === IdentityProvider.SAML)
) {
await prisma.user.update({
where: { email: existingUserWithEmail.email },
data: { password: null },
});
if (existingUserWithEmail.twoFactorEnabled) {
return loginWithTotp(existingUserWithEmail);
} else {
return true;
}
} else if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
return "/auth/error?error=use-password-login";
}
return "/auth/error?error=use-identity-login";
}
const newUser = await prisma.user.create({
data: {
// Slugify the incoming name and append a few random characters to
// prevent conflicts for users with the same name.
username: usernameSlug(user.name),
emailVerified: new Date(Date.now()),
name: user.name,
email: user.email,
identityProvider: idP,
identityProviderId: String(user.id),
},
});
const linkAccountNewUserData = { ...account, userId: newUser.id };
await calcomAdapter.linkAccount(linkAccountNewUserData);
if (account.twoFactorEnabled) {
return loginWithTotp(newUser);
} else {
return true;
}
}
return false;
},
async redirect({ url, baseUrl }) {
// Allows relative callback URLs
if (url.startsWith("/")) return `${baseUrl}${url}`;
// Allows callback URLs on the same domain
else if (new URL(url).hostname === new URL(WEBAPP_URL).hostname) return url;
return baseUrl;
},
},
});
export default NextAuth(AUTH_OPTIONS);
+5 -5
View File
@@ -1,14 +1,14 @@
import { IdentityProvider } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { ErrorCode, hashPassword, verifyPassword } from "../../../lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
const session = await getServerSession({ req, res });
if (!session || !session.user || !session.user.email) {
res.status(401).json({ message: "Not authenticated" });
+18 -1
View File
@@ -1,14 +1,31 @@
import type { ResetPasswordRequest } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { sendPasswordResetEmail } from "@calcom/emails";
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
import rateLimit from "@calcom/lib/rateLimit";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
const limiter = rateLimit({
intervalInMs: 60 * 1000, // 1 minute
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const t = await getTranslation(req.body.language ?? "en", "common");
const email = z
.string()
.email()
.transform((val) => val.toLowerCase())
.parse(req.body?.email);
const { isRateLimited } = limiter.check(10, email); // 10 requests per minute
if (isRateLimited) {
return res.status(429).json({ message: "Too Many Requests." });
}
if (req.method !== "POST") {
return res.status(405).end();
@@ -17,7 +34,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
try {
const maybeUser = await prisma.user.findUnique({
where: {
email: req.body?.email?.toLowerCase(),
email,
},
select: {
name: true,
+1 -2
View File
@@ -1,9 +1,8 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import prisma from "@calcom/prisma";
import { hashPassword } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(400).json({ message: "" });
+5 -1
View File
@@ -5,8 +5,12 @@ import { defaultHandler, defaultResponder } from "@calcom/lib/server";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { oauthController } = await jackson();
const { redirect_url } = await oauthController.samlResponse(req.body);
if (redirect_url) return res.redirect(302, redirect_url);
if (redirect_url) {
res.redirect(302, redirect_url);
}
}
export default defaultHandler({
+2 -2
View File
@@ -2,8 +2,8 @@ import { IdentityProvider } from "@prisma/client";
import type { NextApiRequest } from "next";
import z from "zod";
import { isPasswordValid } from "@calcom/lib/auth";
import { hashPassword } from "@calcom/lib/auth";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import slugify from "@calcom/lib/slugify";
+1 -1
View File
@@ -1,7 +1,7 @@
import { IdentityProvider } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/lib/auth";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import slugify from "@calcom/lib/slugify";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
@@ -1,17 +1,18 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { ErrorCode, getSession, verifyPassword } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
@@ -1,17 +1,17 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { ErrorCode, getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
@@ -3,17 +3,18 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import qrcode from "qrcode";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import { symmetricEncrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { ErrorCode, getSession, verifyPassword } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
+2 -3
View File
@@ -1,14 +1,13 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import notEmpty from "@calcom/lib/notEmpty";
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (!session?.user?.id) {
res.status(401).json({ message: "Not authenticated" });
+4 -4
View File
@@ -1,11 +1,11 @@
import type { NextApiRequest } from "next";
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
import { getSession } from "@calcom/lib/auth";
import { defaultResponder } from "@calcom/lib/server";
async function handler(req: NextApiRequest & { userId?: number }) {
const session = await getSession({ req });
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
const session = await getServerSession({ req, res });
/* To mimic API behavior and comply with types */
req.userId = session?.user?.id || -1;
const booking = await handleNewBooking(req, {
+4 -4
View File
@@ -1,11 +1,11 @@
import type { NextApiRequest } from "next";
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
import { getSession } from "@calcom/lib/auth";
import { defaultResponder, defaultHandler } from "@calcom/lib/server";
async function handler(req: NextApiRequest & { userId?: number }) {
const session = await getSession({ req });
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
const session = await getServerSession({ req, res });
/* To mimic API behavior and comply with types */
req.userId = session?.user?.id || -1;
return await handleCancelBooking(req);
-20
View File
@@ -1,20 +0,0 @@
import type { NextRequest } from "next/server";
export const config = {
runtime: "edge",
};
export default async function handler(req: NextRequest) {
const countryCode = req.headers.get("x-vercel-ip-country") ?? "";
return new Response(
JSON.stringify({
countryCode,
}),
{
status: 200,
headers: {
"content-type": "application/json",
},
}
);
}
+1 -1
View File
@@ -60,7 +60,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write(
renderEmail("OrganizerScheduledEmail", {
renderEmail("OrganizerRequestEmail", {
calEvent: evt,
attendee: evt.organizer,
})
+2 -2
View File
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import type { Session } from "next-auth";
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
import { getSession } from "@calcom/lib/auth";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
import prisma from "@calcom/prisma";
@@ -42,7 +42,7 @@ const defaultIntegrationAddHandler = async ({
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Check that user is authenticated
req.session = await getSession({ req });
req.session = await getServerSession({ req, res });
const { args } = req.query;
+2 -2
View File
@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { performance } from "@calcom/lib/server/perfObserver";
let isCold = true;
@@ -9,7 +9,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const prePrismaDate = performance.now();
const prisma = (await import("@calcom/prisma")).default;
const preSessionDate = performance.now();
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (!session) return res.status(409).json({ message: "Unauthorized" });
const preUserDate = performance.now();
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
+7 -5
View File
@@ -1,11 +1,10 @@
import { ImageResponse } from "@vercel/og";
import type { NextApiRequest } from "next";
import { remark } from "remark";
import type { SatoriOptions } from "satori";
import strip from "strip-markdown";
import { z } from "zod";
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
import { md } from "@calcom/lib/markdownIt";
const calFont = fetch(new URL("../../../../public/fonts/cal.ttf", import.meta.url)).then((res) =>
res.arrayBuffer()
@@ -75,10 +74,13 @@ export default async function handler(req: NextApiRequest) {
meetingImage: searchParams.get("meetingImage"),
imageType,
});
const title_ = md.render(title).replace(/(<([^>]+)>)/gi, "");
const img = new ImageResponse(
(
<Meeting
title={title}
title={title_}
profile={{ name: meetingProfileName, image: meetingImage }}
users={names.map((name, index) => ({ name, username: usernames[index] }))}
/>
@@ -108,8 +110,8 @@ export default async function handler(req: NextApiRequest) {
description: searchParams.get("description"),
imageType,
});
const description_ = await (await remark().use(strip).process(description)).toString();
const img = new ImageResponse(<Generic title={title} description={description_} />, ogConfig) as {
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig) as {
body: Buffer;
};
+7 -2
View File
@@ -3,8 +3,9 @@
*/
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import * as trpcNext from "@calcom/trpc/server/adapters/next";
import { createContext } from "@calcom/trpc/server/createContext";
import { createContext as createTrpcContext } from "@calcom/trpc/server/createContext";
import { appRouter } from "@calcom/trpc/server/routers/_app";
export default trpcNext.createNextApiHandler({
@@ -12,7 +13,11 @@ export default trpcNext.createNextApiHandler({
/**
* @link https://trpc.io/docs/context
*/
createContext,
createContext: ({ req, res }) => {
const sessionGetter = () => getServerSession({ req, res });
return createTrpcContext({ req, res }, sessionGetter);
},
/**
* @link https://trpc.io/docs/error-handling
*/
+1 -1
View File
@@ -2,7 +2,7 @@ import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import prisma from "@calcom/prisma";
import { defaultAvatarSrc } from "@lib/profile";
+4 -4
View File
@@ -1,13 +1,13 @@
import type { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import RoutingFormsRoutingConfig from "@calcom/app-store/ee/routing-forms/pages/app-routing.config";
import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
import type { AppProps } from "@lib/app-providers";
import { getSession } from "@lib/auth";
import { ssrInit } from "@server/lib/ssr";
@@ -103,7 +103,7 @@ export async function getServerSideProps(
appPages?: string[];
}>
) {
const { params } = context;
const { params, req, res } = context;
if (!params) {
return {
notFound: true,
@@ -120,7 +120,7 @@ export async function getServerSideProps(
// appPages is actually hardcoded here and no matter the fileName the same variable would be used.
// We can write some validation logic later on that ensures that [...appPages].tsx file exists
params.appPages = pages.slice(1);
const session = await getSession({ req: context.req });
const session = await getServerSession({ req, res });
const user = session?.user;
const result = await route.getServerSideProps(
+24 -6
View File
@@ -3,9 +3,9 @@ import matter from "gray-matter";
import MarkdownIt from "markdown-it";
import type { GetStaticPaths, GetStaticPropsContext } from "next";
import path from "path";
import { z } from "zod";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import ExisitingGoogleCal from "@calcom/app-store/googlevideo/components/ExistingGoogleCal";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -14,6 +14,23 @@ import App from "@components/apps/App";
const md = new MarkdownIt("default", { html: true, breaks: true });
const sourceSchema = z.object({
content: z.string(),
data: z.object({
description: z.string().optional(),
items: z
.array(
z.union([
z.string(),
z.object({
iframe: z.object({ src: z.string() }),
}),
])
)
.optional(),
}),
});
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
return (
<App
@@ -34,13 +51,13 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
email={data.email}
licenseRequired={data.licenseRequired}
isProOnly={data.isProOnly}
images={source.data?.items as string[] | undefined}
descriptionItems={source.data?.items as string[] | undefined}
isTemplate={data.isTemplate}
dependencies={data.dependencies}
// tos="https://zoom.us/terms"
// privacy="https://zoom.us/privacy"
body={
<>
{data.slug === "google-meet" && <ExisitingGoogleCal />}
<div dangerouslySetInnerHTML={{ __html: md.render(source.content) }} />
</>
}
@@ -86,10 +103,11 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
source = singleApp.description;
}
const { content, data } = matter(source);
const result = matter(source);
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
if (data.items) {
data.items = data.items.map((item: string) => {
if (!item.includes("/api/app-store")) {
data.items = data.items.map((item) => {
if (typeof item === "string" && !item.includes("/api/app-store")) {
// Make relative paths absolute
return `/api/app-store/${appDirname}/${item}`;
}
+4 -2
View File
@@ -3,8 +3,8 @@ import type { ChangeEventHandler } from "react";
import { useState } from "react";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { classNames } from "@calcom/lib";
import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/client";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
@@ -86,9 +86,11 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
const ssg = await ssgInit(context);
const session = await getSession(context);
const session = await getServerSession({ req, res });
let appStore;
if (session?.user?.id) {
+3 -1
View File
@@ -2,6 +2,7 @@ import type { ResetPasswordRequest } from "@prisma/client";
import debounce from "lodash/debounce";
import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Link from "next/link";
import React, { useMemo } from "react";
@@ -21,7 +22,7 @@ type Props = {
export default function Page({ resetPasswordRequest, csrfToken }: Props) {
const { t } = useLocale();
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<{ message: string } | null>(null);
const [, setError] = React.useState<{ message: string } | null>(null);
const [success, setSuccess] = React.useState(false);
const [password, setPassword] = React.useState("");
@@ -185,6 +186,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
},
id,
csrfToken: await getCsrfToken({ req: context.req }),
...(await serverSideTranslations(context.locale || "en", ["common"])),
},
};
} catch (reason) {
+10 -6
View File
@@ -1,16 +1,16 @@
import debounce from "lodash/debounce";
import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Link from "next/link";
import { useRouter } from "next/router";
import type { SyntheticEvent } from "react";
import React from "react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, EmailField } from "@calcom/ui";
import { getSession } from "@lib/auth";
import AuthContainer from "@components/ui/AuthContainer";
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
@@ -132,17 +132,21 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
);
}
ForgotPassword.getInitialProps = async (context: GetServerSidePropsContext) => {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (session) {
res.writeHead(302, { Location: "/" });
res.end();
return;
return { props: {} };
}
return {
csrfToken: await getCsrfToken(context),
props: {
csrfToken: await getCsrfToken(context),
...(await serverSideTranslations(context.locale || "en", ["common"])),
},
};
};
+5 -3
View File
@@ -9,8 +9,9 @@ import { FormProvider, useForm } from "react-hook-form";
import { FaGoogle } from "react-icons/fa";
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { ErrorCode, getSession } from "@calcom/lib/auth";
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -220,8 +221,9 @@ export default function Login({
// TODO: Once we understand how to retrieve prop types automatically from getServerSideProps, remove this temporary variable
const _getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) {
const { req } = context;
const session = await getSession({ req });
const { req, res } = context;
const session = await getServerSession({ req, res });
const ssr = await ssrInit(context);
const verifyJwt = (jwt: string) => {
+1 -1
View File
@@ -43,7 +43,7 @@ export default function Logout(props: Props) {
</div>
</div>
</div>
<Button href="/auth/login" passHref className="flex w-full justify-center">
<Button href="/auth/login" className="flex w-full justify-center">
{t("go_back_login")}
</Button>
</AuthContainer>
+23
View File
@@ -0,0 +1,23 @@
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
// To handle the IdP initiated login flow callback
export default function Page() {
const router = useRouter();
useEffect(() => {
if (!router.isReady) {
return;
}
const { code } = router.query;
signIn("saml-idp", {
callbackUrl: "/",
code,
});
}, []);
return null;
}
+5 -3
View File
@@ -4,8 +4,8 @@ import { useRouter } from "next/router";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
@@ -136,10 +136,12 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
const ssr = await ssrInit(context);
const userCount = await prisma.user.count();
const { req } = context;
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
return {
+5 -2
View File
@@ -1,6 +1,7 @@
import type { GetServerSidePropsContext } from "next";
import { getProviders, signIn, getSession, getCsrfToken } from "next-auth/react";
import { getProviders, signIn, getCsrfToken } from "next-auth/react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { Button } from "@calcom/ui";
type Provider = {
@@ -25,7 +26,9 @@ function signin({ providers }: { providers: Provider[] }) {
export default signin;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const session = await getSession(context);
const { req, res } = context;
const session = await getServerSession({ req, res });
const csrfToken = await getCsrfToken(context);
const providers = await getProviders();
if (session) {
+3 -3
View File
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
import { useEffect } from "react";
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import stripe from "@calcom/features/ee/payments/server/stripe";
import {
hostedCal,
@@ -16,7 +17,6 @@ import { checkUsername } from "@calcom/lib/server/checkUsername";
import prisma from "@calcom/prisma";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import { ssrInit } from "@server/lib/ssr";
@@ -60,9 +60,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
throw new Error(`File is not named sso/[provider]`);
}
const { req } = context;
const { req, res } = context;
const session = await getSession({ req });
const session = await getServerSession({ req, res });
const ssr = await ssrInit(context);
if (session) {

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