Compare commits

..
88 Commits
Author SHA1 Message Date
Alex van Andel 72629ffee0 v2.7.7
release.yml / release (push) Canceled after 0s
2023-04-03 19:47:10 +02:00
Nafees Nazik 5b6f007d8f fix: flickering of tips card rendering while changing routes (#8065)
* feat: optimize tips component

* fix: use dynamic rendering

* fix: remove unnecessary memo

* fix: prefetch warning

* fix: use reverse list
2023-04-03 19:25:45 +02:00
Joe Au-YeungandAlex van Andel 2e0951d4dc Set GCal & Outlook iCalUID in .ics file (#8010)
* Add calendar UID to calendar event

* Add iCalUID to booking event

* On reschedule write to evt the iCalUID

* Add uid to ics file

* Remove console logs

* Pass iCalUID if available

* Remove generated app store files

* Rename product id to calcom

* Add UID to ics on reschedule

* Type fixes

* Type fixes

* Type fixes

* Remove comment

* Remove console.log

* Removed serverConfig block from this branch

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-03 17:13:57 +00:00
Richard Poelderl fd84b5754d chore: update robots.txt to allow api/app-store (#8023) 2023-04-03 16:43:21 +02:00
Udit TakkarandAlex van Andel 73afb03308 fix: remove validateSlug Query (#8015)
* fix: remove validateSlug Query

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

* chore

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: Alex van Andel <me@alexvanandel.com>
2023-04-03 16:37:58 +02:00
Jeroen ReumkensandAlex van Andel 99dba73d0b Updated Vercel OG to latest + added fallback img (#8057)
* Updated Vercel OG to latest so we can add a fallback image — and added this fallback image to the screenshot og image.

* Updated vercel/og in web package as well.

* Updated yarn.lock with auth dependencies

* Upgraded next to latest to fix next image throwing weird bad response errors.

* Revert "Upgraded next to latest to fix next image throwing weird bad response errors."

This reverts commit 4e9d97dbe5ebe682b905ce41f88197915e19647d.

* Added content type headers to og image api response.

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-03 16:06:02 +02:00
Carina WollendorferandCarinaWolli db4449d849 fix double new button (#8058)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-04-03 14:34:53 +02:00
Nafees Nazik 53e52a0510 fix: flash of grey in event types single page (#8056)
* feat: prefetch query

* fix: hydration error

* fix: add a skeleton loader
2023-04-03 14:33:36 +02:00
Peer Richelsen cf4bd8a77d yarn format 2023-04-03 14:08:17 +02:00
Nafees Nazik 9d61dc2f7c fix: flash of grey when navigating to workflow page (#8038)
* fix: flash of grey

* fix: remove rendering null
2023-04-03 11:50:39 +02:00
Carina WollendorferandCarinaWolli caf5918b72 Delete canceled workflow emails from scheduled_sends (SendGrid endpoint) (#8026)
* show already verified numbers as verified

* delete old batch_ids from scheduled sends

* only delete cancelled ones

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-04-03 11:25:20 +02:00
Hariom Balhara 84f64f5d06 Show correct label in email and make sure that rechedule reason is shown on booking success page (#8053) 2023-04-03 11:18:12 +02:00
GitStart-Cal.comandgitstart-calcom 13a53c020b [CAL-1094] Embed modal / Inline - UI/Layout/Spacing issues (#7350)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-04-03 10:50:44 +05:30
GermaVinsmoke a72f90fe02 fix: misaligned booking frequency toggle (#8006) 2023-04-02 18:35:10 +02:00
Sai Deepesh 283d5b4a65 refactor: replace TextWidget component with EmailField (#8047) 2023-04-02 18:34:11 +02:00
Nafees Nazik 81b2e37efd fix: add calendar button layout shift (#7930) 2023-04-02 10:34:48 +00:00
Nafees NazikandPeer Richelsen 469967b320 fix: disable 2FA switch for non CAL auth identity providers (#8021)
* fix: disable switch on non cal providers

* feat: extract to an i18n key

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

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-02 10:31:40 +00:00
d708625da0 Update packages/features/ee/teams/components/MemberListItem.tsx (#8030)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-02 10:26:22 +00:00
Udit Takkar acf2487eaa fix: use ssrinit in /apps (#7807)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-04-02 12:23:30 +02:00
Peer Richelsen b81ca0e2ab renamed banner folder to prevent adblock from blocking (#8020) 2023-04-02 12:22:31 +02:00
GitStart-Cal.comandgitstart-calcom 2357f76cc8 CALCOM-8011 - [CAL-1372] /workflows: Filters avatars not showing (#8036)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-04-02 10:21:58 +00:00
Nafees Nazik 304e8aaa68 fix: modal padding on mobile (#8002)
* fix: width full

* fix: max width
2023-04-02 12:10:30 +02:00
Nafees Nazik 9fe912c392 feat: soft delete rescheduled booking (#7970)
* feat: add rescheduled status

* feat: add new reservation status

* fix: add rescheduled to trpc procedure

* feat: use soft delete

* feat: add new i18n key

* feat: add data display for rescheduled booking

* chore: fix comment

* Revert "feat: add new i18n key"

This reverts commit 4cecc00bd3351af6fbdff05d728c438f68fbfc4e.

* Revert "feat: add data display for rescheduled booking"

This reverts commit 81b4f7e1ab24763f630428b6c338926cf1958be0.

* Revert "fix: add rescheduled to trpc procedure"

This reverts commit 82b26582d2188e2b18f2f733760c741e7127b1d2.

* Revert "feat: add new reservation status"

This reverts commit b1dafab774d566dfcbe1c979035a89c729150d59.

* Revert "feat: add rescheduled status"

This reverts commit c8f24aec0e054b6be7186a631eee2b8a36ced40f.

* fix: use cancelled

* fix: e2e

* fix: e2e

* fix: e2e
2023-04-02 12:09:57 +02:00
Nafees Nazik ab7ecff580 fix: use dropdown item (#8024) 2023-04-02 12:03:40 +02:00
GitStart-Cal.comandgitstart-calcom 19e830acad CALCOM-7983 - [CAL-1359] /teams: Members should not see "Next steps" since they can't take any (#8029)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-04-02 12:03:28 +02:00
Nafees Nazik 458300bc83 fix: use the correct step (#8037) 2023-04-01 22:14:08 +02:00
Gurkiran Singh 352a467104 feat: Throwing a toast when adding a new team member (#8046)
Signed-off-by: GurkiranSingh <gurkiransinghk@gmail.com>
2023-04-01 22:12:48 +02:00
c052c66abc chore: removed duplicate destination calendar selector components (#7880)
* Deleted DestinationCalendarSelector

* Renamed to DestinationCalendarSelectorComp

* Delete DestinationCalendarSelectorComp instead of rename

* Add DestinationCalendarSelect to CreateEventsOnCalendar

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-31 21:21:00 +00:00
Joe Au-YeungandOmar López 8f8ed4d80d Show proper default destination calendar (#7644)
* Make destination calendar immutable

* Add comment

---------

Co-authored-by: Omar López <zomars@me.com>
2023-03-31 23:12:28 +02:00
Udit TakkarandPeer Richelsen 04fb7e07b4 fix: change ui of booking fields in booking page (#7776)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-31 17:15:09 +00:00
Nafees Nazik 20d2129f81 hotfix: locale not changeable (#7981)
* fix: export type

* feat: extract into a utility

* feat: use the new utility

* fix: comment out authedRateLimitedProcedure

* fix: type error

* fix: remove spread
2023-03-31 11:38:46 -03:00
Joe Au-Yeung ae5425f908 Fix showing seat attendees in emails (#8033)
* Pass seats info to location change

* Fix cloning emails

* Add cloning of event in AttendeeScheduledEmail class
2023-03-30 19:45:48 -04:00
Peer Richelsen 762810684e Update common.json 2023-03-30 21:20:37 +02:00
Omar López 4720dc410f Fixes embeds tsconfig (#7995)
* Update tsconfig.json

* Fixes?

* Fixes
2023-03-30 16:20:39 +00:00
Joe Au-Yeung 715aea5733 Refactor Prisma call creating attendees and seat reference (#8014)
* Refactor prisma calls

* Reverted change to pass test
2023-03-29 23:13:35 +02:00
GitStart-Cal.comandgitstart-calcom 6cbb9bf967 CALCOM-7857 - [CAL-1303] UI issues webhook list (#7968)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-03-29 07:44:45 +02:00
Omar López d8176b2eb4 v2.7.6
release.yml / release (push) Canceled after 0s
2023-03-28 16:28:17 -07:00
6c51e2a970 insights follow up (#7922)
* init page

* init insights frontend

* nit

* nit

* nit

* merge

* fixed icons

* i18n, needs features

* Init insights trpc

* Using trpc on client

* Added events timeline

* Seed analytics script

* connect ui with trpc

* Added and fixed event time lines

* WIP popular days and avg time duration event type

* added new metric graphs

* improved upgrade tip

* always show upgrade screen

* upgrade tremor.so and select inputs for page

* Remove log

* Move everything to components and add context

* Fix select types using calcom ui one

* Adding translations

* Add missing translations

* Add more translations

* min fix

* Fixes for date select

* Prefer early return and mobile design fixes

* Fix style for mobile

* Fix data with userId filter from popular events

* add user id to average time duration

* fix types for select-react

* Removed submodules

* Delete website

* Update yarn.lock

* Code organization and type fixes

* trpc fixes

* Builds are now passing

* Relocates server code

* Add url state in insights

* Update FiltersProvider.tsx

* Cleanup

* Update embed-iframe.ts

* Update FilterType.tsx

* Update seed-app-store.config.json

* Update index.tsx

* Renamed seeder

* Update FiltersProvider.tsx

* Fix for query params

* no wrap on lg screen

* Fix shadow borders from tremor components, fix title font

* Add ring-gray to match filters

* add cursor pointer

* copy improvements

* blue to black

* fixed date select focus

* Adds missing translation strings

* Fix url state for filter type

* Apply suggestions from code review

* Updated yarn lock

* Adds feature flag

* Type fix

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-03-28 16:24:57 -07:00
zomars 3a08eb3dca Adds pagination 2023-03-28 15:11:27 -07:00
cee905bc5a Improves reported type errors on CI (#7813)
* 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

* Adds type check special for CI

* Update turbo.json

* Update turbo.json

---------

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-28 13:12:40 -07:00
72fd6334c6 Add the new Team->useHostDefault option (#7811)
* Add the new Team->useHostDefault option

* Added user.metadata to defaultEvents

* Type fixes

* Another type fix

* Implement feedback @jaibles

* Default -> Conferencing rename

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-03-28 20:03:54 +00:00
Omar López 63f51abd84 Adds cron endpoint to keep team subscriptions in sync (#7992)
* Fix missing edge case where team is paid but needs to be updated

* Reuses cron endpoint to update team subscriptions

* Update cron-downgradeUsers.yml

* Update payments.ts

* Update cron-downgradeUsers.yml

* update to 90 writes per second

Stripe allow up to 100 per second
2023-03-28 12:38:01 -07:00
Omar López 6e856b3bd1 Fix missing edge case where team is paid but needs to be updated (#7988) 2023-03-28 18:45:13 +00:00
Hariom Balhara 5fceee27fe Embed: Strictly type the codebase and fixes a few bugs found (#7536)
* Add strict types

* Make it array

* Make api consistent and support calOrigin in floatingPopup and modal

* Support calOrigin everywhere and fix branding config issue

* Fix styles not being uniformly applied across components

* Fix unused code
2023-03-28 11:17:40 -07:00
Alex van Andel 4f590ebbf7 Remove 'avatar' (which is unused) from eventType.hosts query (#7986) 2023-03-28 18:44:30 +02:00
Hariom Balhara 736cd63509 Fix: AppStore CLI Create command not working after upgrading to yarn 3 (#7975)
* Remove README.md that is now replaced by DESCRIPTION/md

* Fix AppStore Cli create command failure
2023-03-28 10:25:51 +00:00
3aaa1cde0d Fixes formatted description in email + sanitize html everywhere (#7928)
* use sanitize-html instead

* add list formatting

* add lost changes from merging main

* fixes caused by merge

* remove dompurify

* Revert "remove dompurify"

This reverts commit 583bb623c7d3c0c7daa28e63ab2b1ac2da68c0d8.

* sanitize already done in editor

* Update yarn.lock

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-28 09:40:13 +00:00
Keith Williams 02d32321aa Updated the PR template to not use checkboxes for 'Type of change' (#7960)
* Updated the PR template to not use checkboxes for 'Type of change'

* Changed wording of options to bullets
2023-03-28 10:43:27 +02:00
GitStart-Cal.comandgitstart-calcom 697f6db2c4 CALCOM-7962 - [CAL-1350] create "collective or round robin" doenst open dialog anymore (#7969)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-03-28 10:36:19 +02:00
Alex van Andel 83364a9e97 Update yarn.lock 2023-03-28 10:30:57 +02:00
Julius Marminge 1620ae660f fix(embeds): add support for RSC by adding use client directives (#7919) 2023-03-28 11:30:02 +05:30
Alex van Andel 605f9c61f3 bookingSeat could absolutely be unset at this point, resulting in Internal Error. (#7964)
release.yml / release (push) Canceled after 0s
2023-03-27 18:16:07 -07:00
Omar López 564862369f v2.7.5 2023-03-27 17:30:40 -07:00
Alex van Andel be6cf4705c An empty array is truthy, [0] potentially isn't (#7961) 2023-03-28 00:16:52 +02:00
Joe Au-YeungandPeer Richelsen a2457ab619 Fix/availability not loading (#7789)
* Set default schedule if there is not one

* Removing last schedule sets user.defaultScheduleId to null

* Create new schedule from event type

* First schedule created is the default

* Go back if new schedule was created

* Moved update logic out of getter

* Remove flashing of empty screen

* Add border prop to EmptyScreen

* User requires a schedule

* Change back schema

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-27 19:53:35 +00:00
Alex van Andel 867966dd75 Fixup whereby config.json, invalid JSON 2023-03-27 21:12:57 +02:00
Peer RichelsenandAlex van Andel 08ad4c07ea added better regex for whereby allowing subdomains (#7957)
* added better regex for whereby allowing subdomains

* Update packages/app-store/whereby/config.json

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

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-27 21:01:34 +02:00
Joe 78da57055e fix: updating password ui bug #7775 (#7815)
* fix: updating password ui bug #7775

* fix: type error
2023-03-27 20:58:46 +02:00
Udit TakkarandPeer Richelsen d5c3d9d967 fix: create team form (#7896)
* fix: create team form

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

* fix: change hover border

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-27 16:35:46 +00:00
aa0e9e6a02 Prevent GCal from sending update (#7920)
* Do not send updates

* Clean up

* Clean up

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

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

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-27 14:22:27 +00:00
Alex van Andel cad97f1779 Remove 3rd param from getBooking 2023-03-27 13:32:35 +02:00
GitStart-Cal.comandgitstart-calcom 31ee689672 CALCOM-7848 - Confirm button also in the branding color (#7932)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-03-27 12:31:17 +02:00
3edb6f5ffd Delete rescheduled booking (#7910)
* deleted orignal booking and fix open seats number on booking page

* fix type error

* Adjust tests

* fix type error

* include fix in e2e test

* fix e2e test

* fixes e2e test

* fix type error in e2e test

* increase timeout time

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2023-03-27 11:34:41 +02:00
Hariom Balhara a8825badec Fix: Send responses in confirm booking flow (#7830) 2023-03-27 13:57:10 +05:30
sean-brydonandEfraín Rochín 04c634ec4b Fix timezone select - create two variants (#7875)
* Fix timezone select - create two variants

* Remove menu is open

* Timezone fixes

---------

Co-authored-by: Efraín Rochín <roae.85@gmail.com>
2023-03-26 21:19:13 +02:00
Joe Au-Yeung 8dd9b8d74b Fix Zod errors in Outlook auth function (#7931)
* Add quick fixes

* Add error message

* Wooops
2023-03-25 21:19:33 +00:00
Efraín Rochín 0365157638 Revalidate booking pages on updateProfile (#7941) 2023-03-25 22:06:43 +01:00
zomars a571d40958 Update yarn.lock 2023-03-24 18:00:50 -07:00
69491d5700 Adds basic global feature flags (#7459)
* Adds basic feature flag model

* Create router.ts

* WIP

* WIP

* WIP

* WIP

* Emails kill switch

* Adds missing migrations

* Type fix

* Cleanup

* Revert

* Cleanup

* Fixes migration

* Update packages/features/flags/server/utils.ts

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

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-03-24 17:59:04 -07:00
Joe Au-Yeung 20022e2cdd Fix minimum booking notice set to 0 (#7750)
* Only use the current day

* Set min of booking notice to 0

* Set min minimumBookingNotice to 0
2023-03-24 17:37:46 -07:00
Syed Ali Shahbaz 492858b45b Fix for Apple Calendar failing entire availability check because of Shared Calendars (#7934) 2023-03-24 23:55:30 +01:00
Bailey Pumfleet b49ad8294b new yarn.lock 2023-03-24 16:43:30 +00:00
Peer Richelsen aa30ff7d1a insights improvements 2023-03-24 00:17:12 +01:00
cfa720a523 /insights (#6511)
* init page

* init insights frontend

* nit

* nit

* nit

* merge

* fixed icons

* i18n, needs features

* Init insights trpc

* Using trpc on client

* Added events timeline

* Seed analytics script

* connect ui with trpc

* Added and fixed event time lines

* WIP popular days and avg time duration event type

* added new metric graphs

* improved upgrade tip

* always show upgrade screen

* upgrade tremor.so and select inputs for page

* Remove log

* Move everything to components and add context

* Fix select types using calcom ui one

* Adding translations

* Add missing translations

* Add more translations

* min fix

* Fixes for date select

* Prefer early return and mobile design fixes

* Fix style for mobile

* Fix data with userId filter from popular events

* add user id to average time duration

* Removed submodules

* Delete website

* Update yarn.lock

* Code organization and type fixes

* trpc fixes

* Builds are now passing

* Relocates server code

* Cleanup

* Medium size screen fixes

* Added TODO

---------

Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-03-23 22:10:01 +00:00
Omar López 9ab196c30e Revert "Fixes formatted description in email " (#7917) 2023-03-23 21:20:35 +00:00
Peer Richelsen 47e13cd2fd Update package.json
release.yml / release (push) Canceled after 0s
2023-03-23 21:18:33 +01:00
Hariom Balhara 74ab6d209e Fix/correctly handle Pending Status and fix conditional require confirmation (#7911)
* Use pending status

* Use requiresConfirmation variable instead of eventType.requiresConfirmation as the earlier handles requiresConfirmation threshold as well
2023-03-23 19:50:39 +01:00
Alex van Andel 46f0b55a45 Techdebt/bundle size experiments (#7894)
* Checking package size effects when we remove zod from middleware

* Remove 'crypto' from client code

* Revert "Checking package size effects when we remove zod from middleware"

This reverts commit ecc2038411c73fc562d57712d5c83d66925e7e13.

* Adding lodash to dynamicImports + consistent imports

* Remove the locales from the global Dayjs object
2023-03-23 11:49:28 -07:00
Alex van Andel e4893c2218 Removes the getUser logic from createContext, instead use isAuthed (#7902)
* Removes the getUser logic from createContext, instead use isAuthed

* Fix types :party
2023-03-23 19:46:33 +01:00
Udit Takkar fa17139e8f fix: add belongsToActiveTeam (#7890)
* fix: add belongsToActiveTeam

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

* fix: create a common function

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

* fix: add suggestion

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

* chore: use deconstructing

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

* chore

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-03-23 19:23:06 +01:00
Efraín Rochín 1cdc97dd37 Add detailed error logs to handleNewBooking (#7907)
* Add logs to get more context on "No user available" error

* remove nit
2023-03-23 19:03:49 +01:00
Hariom Balhara f630e8af40 Add videocallData (#7899) 2023-03-23 15:18:27 +00:00
Peer Richelsen fe0745121b minor fix to adding to badge 2023-03-23 13:07:11 +01:00
f6d7568d0a Fixes formatted description in email (#7696)
* fix description in email

* add styling for lists

* sanitize editor input before saving

* sanitize eventTypeDescription

* santize html when used dangerouslySetInnerHTML

* fix server side sanitation

* add missing formatting and sanitation

* move @ts-expect-error to correct line

* fix type error and add yarn.lock

* fix build error

* sanitize description for booking page and availability page

* rename to markdownAndSanitize

* always add list formatting

* handle empty description in markdownAndSanitize for cleaner code

* create function for clientside markdown rendering and sanitizing

* fix type error

* code clean up

* Now that eventType.descriptionAsSafeHTML is added at all the missing places, we can do away with ts-ignore and get type safety

* Remove unused variable

* Remove one more ts-expect-error

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-23 12:00:42 +00:00
Nafees Nazik 520e7fe036 fix: remove "Nameless" from title in booking cancel page (#7888) 2023-03-23 10:22:30 +00:00
Nafees Nazik 1bf50e09cd fix: meeting not getting deleted (#7878) 2023-03-22 11:18:00 -07:00
darshanandBailey Pumfleet d1ca7b4bdd fix redirect for recurring events (#7821)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-03-22 11:22:40 -04:00
Richard Poelderl 4e2473531f fix: set canonical to https://cal.com on .dev as well (#7883) 2023-03-22 15:43:59 +01:00
226 changed files with 5831 additions and 2331 deletions

No files matched your search

+6 -6
View File
@@ -12,13 +12,13 @@ Fixes # (issue)
## Type of change ## Type of change
<!-- Please delete options that are not relevant. --> <!-- Please delete bullets that are not relevant. -->
- [ ] Bug fix (non-breaking change which fixes an issue) - Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements) - Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality) - New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update - This change requires a documentation update
## How should this be tested? ## How should this be tested?
+18
View File
@@ -0,0 +1,18 @@
{
"problemMatcher": [
{
"owner": "tsc-absolute",
"pattern": [
{
"regexp": "(?:^|\\s)([^\\s].*)[\\(:](\\d+)[,:](\\d+)(?:\\):\\s+|\\s+-\\s+)(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"code": 5,
"message": 6
}
]
}
]
}
+5 -3
View File
@@ -10,8 +10,10 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-install
- name: Show info - name: Show info
run: node -e "console.log(require('v8').getHeapStatistics())" run: node -e "console.log(require('v8').getHeapStatistics())"
- name: Configure TSC problem matcher
- run: yarn type-check run: |
echo "::remove-matcher owner=tsc::"
echo "::add-matcher::.github/matchers/tsc-absolute.json"
- run: yarn type-check:ci
+3 -2
View File
@@ -1,11 +1,12 @@
name: Cron - downgradeUsers name: Cron - downgradeUsers
on: on:
workflow_dispatch:
# "Scheduled workflows run on the latest commit on the default or base branch." # "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule # — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule: schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru) # Runs “Every month at 1st (see https://crontab.guru)
- cron: "0,15,30,45 * * * *" - cron: "0 0 1 * *"
jobs: jobs:
cron-downgradeUsers: cron-downgradeUsers:
env: env:
-1
View File
@@ -352,7 +352,6 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
</picture> </picture>
</a> </a>
<!-- CONTRIBUTORS --> <!-- CONTRIBUTORS -->
### Contributors ### Contributors
+2 -1
View File
@@ -11,7 +11,8 @@
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix", "lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json", "lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
"start": "PORT=4000 next start", "start": "PORT=4000 next start",
"type-check": "tsc --pretty --noEmit" "type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit"
}, },
"author": "Cal.com, Inc.", "author": "Cal.com, Inc.",
"license": "MIT", "license": "MIT",
@@ -1,86 +0,0 @@
import React from "react";
import Select from "react-select";
import type { OptionProps } from "react-select";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App";
import { Button } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
interface AdditionalCalendarSelectorProps {
isLoading?: boolean;
}
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
const { data } = optionProps;
return (
<InstallAppButton
type={data.type}
render={(installProps) => {
return (
<Button {...installProps} className="w-full" color="minimal">
{/* eslint-disable @next/next/no-img-element */}
{data.image && (
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
)}
<p>{data.label}</p>
</Button>
);
}}
/>
);
};
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
return (
<QueryCell
query={query}
success={({ data }) => {
const options = data.items.map((item) => ({
label: item.name,
slug: item.slug,
image: item.logo,
type: item.type,
}));
return (
<Select
name="additionalCalendar"
placeholder={t("connect_additional_calendar")}
options={options}
styles={{
placeholder: (defaultStyles) => {
return {
...defaultStyles,
color: "#3E3E3E",
marginLeft: "3px",
};
},
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "2px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
maxWidth: "320px",
},
};
},
}}
isSearchable={false}
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm font-medium text-gray-700"
isLoading={isLoading}
components={{ Option: ImageOption }}
/>
);
}}
/>
);
};
export default AdditionalCalendarSelector;
@@ -1,137 +0,0 @@
import classNames from "classnames";
import React, { useEffect, useState } from "react";
import Select from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { DestinationCalendar } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
interface Props {
onChange: (value: { externalId: string; integration: string }) => void;
isLoading?: boolean;
hidePlaceholder?: boolean;
/** The external Id of the connected calendar */
value: string | undefined;
destinationCalendar?: DestinationCalendar | null;
}
const DestinationCalendarSelector = ({
onChange,
isLoading,
value,
hidePlaceholder,
destinationCalendar,
}: Props): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.connectedCalendars.useQuery();
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
// Extra styles to show prefixed text in react-select
const content = (hidePlaceholder = false) => {
if (!hidePlaceholder) {
return {
alignItems: "center",
display: "flex",
":before": {
content: `'${t("select_destination_calendar")}:'`,
display: "block",
marginRight: 8,
},
};
}
return {};
};
useEffect(() => {
const selected = query.data?.connectedCalendars
.map((connected) => connected.calendars ?? [])
.flat()
.find((cal) => cal.externalId === value);
if (selected) {
setSelectedOption({
value: `${selected.integration}:${selected.externalId}`,
label: selected.name || "",
});
}
}, [query.data?.connectedCalendars, value]);
if (!query.data?.connectedCalendars.length) {
return null;
}
const options =
query.data.connectedCalendars.map((selectedCalendar) => ({
key: selectedCalendar.credentialId,
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
options: (selectedCalendar.calendars ?? [])
.filter((cal) => cal.readOnly === false)
.map((cal) => ({
label: cal.name || "",
value: `${cal.integration}:${cal.externalId}`,
})),
})) ?? [];
const defaultCalendarSelectedString = destinationCalendar?.externalId
? `(${
destinationCalendar.externalId.length > 15
? destinationCalendar.externalId.substring(0, 15) + "..."
: destinationCalendar.externalId
})`
: "";
return (
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
<Select
name="primarySelectedCalendar"
placeholder={
!hidePlaceholder ? (
`${t("select_destination_calendar")}`
) : (
<span className="whitespace-nowrap">
{t("default_calendar_selected")} {defaultCalendarSelectedString}
</span>
)
}
options={options}
styles={{
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "6px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
width: "100%",
},
};
},
}}
isSearchable={false}
className={classNames(
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
!hidePlaceholder && "font-medium"
)}
onChange={(option) => {
setSelectedOption(option);
if (!option) {
return;
}
/* Split only the first `:`, since Apple uses the full URL as externalId */
const [integration, externalId] = option.value.split(/:(.+)/);
onChange({
integration,
externalId,
});
}}
isLoading={isLoading}
value={selectedOption}
components={{
IndicatorSeparator: () => null,
}}
/>
</div>
);
};
export default DestinationCalendarSelector;
+56 -69
View File
@@ -1,4 +1,4 @@
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
import classNames from "classnames"; import classNames from "classnames";
import type { NextRouter } from "next/router"; import type { NextRouter } from "next/router";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -22,7 +22,7 @@ import {
TextField, TextField,
ColorPicker, ColorPicker,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon"; import { FiCode, FiTrello, FiSun, FiArrowLeft } from "@calcom/ui/components/icon";
import Select from "@components/ui/form/Select"; import Select from "@components/ui/form/Select";
@@ -167,7 +167,7 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
const height = getDimension(previewState.inline.height); const height = getDimension(previewState.inline.height);
return code` return code`
import Cal, { getCalApi } from "@calcom/embed-react"; import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function MyComponent() { function MyComponent() {
useEffect(()=>{ useEffect(()=>{
(async function () { (async function () {
@@ -187,12 +187,12 @@ function MyComponent() {
}) => { }) => {
return code` return code`
import Cal, { getCalApi } from "@calcom/embed-react"; import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function MyComponent() { function MyComponent() {
useEffect(()=>{ useEffect(()=>{
(async function () { (async function () {
const cal = await getCalApi(); const cal = await getCalApi();
Cal("floatingButton", ${floatingButtonArg}); cal("floatingButton", ${floatingButtonArg});
${uiInstructionCode} ${uiInstructionCode}
})(); })();
}, []) }, [])
@@ -201,7 +201,7 @@ function MyComponent() {
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => { "element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
return code` return code`
import Cal, { getCalApi } from "@calcom/embed-react"; import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function MyComponent() { function MyComponent() {
useEffect(()=>{ useEffect(()=>{
(async function () { (async function () {
@@ -313,7 +313,7 @@ const embeds: {
}[] = [ }[] = [
{ {
title: "Inline Embed", title: "Inline Embed",
subtitle: "Loads your Cal scheduling page directly inline with your other website content", subtitle: "Loads your event type directly inline with your other website content.",
type: "inline", type: "inline",
illustration: ( illustration: (
<svg <svg
@@ -798,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
} }
const ThemeOptions = [ const ThemeOptions = [
{ value: Theme.auto, label: "Auto Theme" }, { value: Theme.auto, label: "Auto" },
{ value: Theme.dark, label: "Dark Theme" }, { value: Theme.dark, label: "Dark Theme" },
{ value: Theme.light, label: "Light Theme" }, { value: Theme.light, label: "Light Theme" },
]; ];
@@ -815,11 +815,18 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
]; ];
return ( return (
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation"> <DialogContent
ref={dialogContentRef}
className="rounded-lg p-0.5 sm:max-w-[80rem]"
enableOverflow
type="creation">
<div className="flex"> <div className="flex">
<div className="flex w-1/3 flex-col bg-gray-50 p-8"> <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"> <h3
className="mb-2.5 flex items-center text-xl font-semibold leading-5 text-gray-900"
id="modal-title">
<button <button
className="h-6 w-6"
onClick={() => { onClick={() => {
removeQueryParams(router, ["embedType", "embedTabName"]); removeQueryParams(router, ["embedType", "embedTabName"]);
}}> }}>
@@ -827,35 +834,21 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</button> </button>
{embed.title} {embed.title}
</h3> </h3>
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} /> <h4 className="mb-6 text-sm font-normal text-gray-500">{embed.subtitle}</h4>
<div className="flex flex-col overflow-y-auto"> <div className="flex flex-col">
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}> <div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible <Collapsible
open={isEmbedCustomizationOpen} open={isEmbedCustomizationOpen}
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}> onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
<CollapsibleTrigger
type="button"
className="flex w-full items-center text-base font-medium text-gray-900">
<div>
{embedType === "inline"
? "Inline Embed Customization"
: embedType === "floating-popup"
? "Floating Popup Customization"
: "Element Click Customization"}
</div>
<FiChevronRight
className={`${
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-gray-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent className="text-sm"> <CollapsibleContent className="text-sm">
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}> <div className={classNames(embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */} {/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-sm">Embed Window Sizing</div> <div className="mb-[9px] text-sm">Window sizing</div>
<div className="justify-left flex items-center"> <div className="justify-left flex items-center !font-normal">
<div className="mr-[9px]">
<TextField <TextField
labelProps={{ className: "hidden" }} labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
required required
value={previewState.inline.width} value={previewState.inline.width}
onChange={(e) => { onChange={(e) => {
@@ -873,9 +866,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
}} }}
addOnLeading={<>W</>} addOnLeading={<>W</>}
/> />
<span className="p-2">×</span> </div>
<TextField <TextField
labelProps={{ className: "hidden" }} labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
value={previewState.inline.height} value={previewState.inline.height}
required required
onChange={(e) => { onChange={(e) => {
@@ -1007,22 +1002,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
</div> </div>
<hr className="mt-4" /> <div className="font-medium">
<div className="mt-4 font-medium">
<Collapsible <Collapsible
open={isBookingCustomizationOpen} open={isBookingCustomizationOpen}
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}> onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleTrigger className="flex w-full" type="button">
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
<FiChevronRight
className={`${
isBookingCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-gray-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="mt-6 text-sm"> <div className="mt-6 text-sm">
<div className="mb-4 flex items-center justify-start space-x-2 rtl:space-x-reverse"> <div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
<Switch <Switch
checked={previewState.hideEventTypeDetails} checked={previewState.hideEventTypeDetails}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
@@ -1036,13 +1022,38 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
/> />
<div className="text-sm">{t("hide_eventtype_details")}</div> <div className="text-sm">{t("hide_eventtype_details")}</div>
</div> </div>
<Label className=""> {[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="mb-6">
<div className="mb-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
<Label>
<div className="mb-2">Theme</div> <div className="mb-2">Theme</div>
<Select <Select
className="w-full" className="w-full"
defaultValue={ThemeOptions[0]} defaultValue={ThemeOptions[0]}
components={{ components={{
Control: ThemeSelectControl, Control: ThemeSelectControl,
IndicatorSeparator: () => null,
}} }}
onChange={(option) => { onChange={(option) => {
if (!option) { if (!option) {
@@ -1058,30 +1069,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
options={ThemeOptions} options={ThemeOptions}
/> />
</Label> </Label>
{[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="pb-4">
<div className="mb-2 pt-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
+1 -1
View File
@@ -1,5 +1,5 @@
import { AdminRequired } from "components/ui/AdminRequired"; import { AdminRequired } from "components/ui/AdminRequired";
import noop from "lodash/noop"; import { noop } from "lodash";
import type { LinkProps } from "next/link"; import type { LinkProps } from "next/link";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -1,10 +1,14 @@
import type { OptionProps } from "react-select";
import { InstallAppButton } from "@calcom/app-store/components"; import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App"; import {
import { Button, Select } from "@calcom/ui"; Button,
Dropdown,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
DropdownItem,
} from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon"; import { FiPlus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
@@ -13,31 +17,6 @@ interface AdditionalCalendarSelectorProps {
isLoading?: boolean; isLoading?: boolean;
} }
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
const { t } = useLocale();
const { data } = optionProps;
return data.slug !== "add-new" ? (
<InstallAppButton
type={data.type}
render={(installProps) => {
return (
<Button {...installProps} className="flex w-full align-top" color="minimal">
{data.image && (
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
)}
<p className="text-left">{`${t("add")} ${data.label}`}</p>
</Button>
);
}}
/>
) : (
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
<FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
<p>{t("install_new_calendar_app")}</p>
</Button>
);
};
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => { const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale(); const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true }); const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
@@ -59,51 +38,39 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
type: "new_other", type: "new_other",
}); });
return ( return (
<Select <Dropdown modal={false}>
name="additionalCalendar" <DropdownMenuTrigger asChild>
placeholder={ <Button StartIcon={FiPlus} color="secondary" {...(isLoading && { loading: isLoading })}>
<Button StartIcon={FiPlus} color="secondary">
{t("add")} {t("add")}
</Button> </Button>
} </DropdownMenuTrigger>
options={options} <DropdownMenuContent>
isSearchable={false} {options.map((data) => (
isLoading={isLoading} <DropdownMenuItem key={data.slug} className="focus:outline-none">
components={{ Option: ImageOption }} {data.slug === "add-new" ? (
styles={{ <DropdownItem StartIcon={FiPlus} color="minimal" href="/apps/categories/calendar">
menu: (defaultStyles) => ({ {t("install_new_calendar_app")}
...defaultStyles, </DropdownItem>
"@media only screen and (max-width: 640px)": { ) : (
left: "0", <InstallAppButton
}, type={data.type}
width: "max-content", render={(installProps) => {
right: "0", const props = { ...installProps } as any;
}), return (
indicatorSeparator: (defaultStyles) => ({ <DropdownItem {...props} color="minimal" type="button">
...defaultStyles, <span className="flex items-center gap-x-2">
display: "none", {data.image && <img className="h-5 w-5" src={data.image} alt={data.label} />}
}), {`${t("add")} ${data.label}`}
control: (defaultStyles) => ({ </span>
...defaultStyles, </DropdownItem>
padding: "0", );
border: "0",
borderRadius: "6px",
minHeight: "auto",
}),
dropdownIndicator: (defaultStyles) => ({
...defaultStyles,
display: "none",
}),
valueContainer: (defaultStyles) => ({
...defaultStyles,
padding: "0",
}),
placeholder: (defaultStyles) => ({
...defaultStyles,
margin: "0",
}),
}} }}
/> />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</Dropdown>
); );
}} }}
/> />
@@ -1,139 +0,0 @@
import classNames from "classnames";
import React, { useEffect, useState } from "react";
import Select from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { DestinationCalendar } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
interface Props {
onChange: (value: { externalId: string; integration: string }) => void;
isLoading?: boolean;
hidePlaceholder?: boolean;
/** The external Id of the connected calendar */
value: string | undefined;
destinationCalendar?: DestinationCalendar | null;
}
const DestinationCalendarSelector = ({
onChange,
isLoading,
value,
hidePlaceholder,
destinationCalendar,
}: Props): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.connectedCalendars.useQuery();
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
// Extra styles to show prefixed text in react-select
const content = (hidePlaceholder = false) => {
if (!hidePlaceholder) {
return {
alignItems: "center",
display: "flex",
":before": {
content: `'${t("select_destination_calendar")}:'`,
display: "block",
marginRight: 8,
},
};
}
return {};
};
useEffect(() => {
const selected = query.data?.connectedCalendars
.map((connected) => connected.calendars ?? [])
.flat()
.find((cal) => cal.externalId === value);
if (selected) {
setSelectedOption({
value: `${selected.integration}:${selected.externalId}`,
label: `${selected.name} (${selected.externalId})` || "",
});
}
}, [query.data?.connectedCalendars, value]);
if (!query.data?.connectedCalendars.length) {
return null;
}
const options =
query.data.connectedCalendars.map((selectedCalendar) => ({
key: selectedCalendar.credentialId,
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
options: (selectedCalendar.calendars ?? [])
.filter((cal) => cal.readOnly === false)
.map((cal) => ({
label: `${cal.name} (${cal.externalId})` || "",
value: `${cal.integration}:${cal.externalId}`,
})),
})) ?? [];
const defaultCalendarSelectedString = destinationCalendar?.externalId
? `(${
destinationCalendar.externalId.length > 15
? destinationCalendar.externalId.substring(0, 15) + "..."
: destinationCalendar.externalId
})`
: "";
return (
<div
className="relative flex items-center"
title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
<Select
name="primarySelectedCalendar"
placeholder={
!hidePlaceholder ? (
`${t("select_destination_calendar")}`
) : (
<span className="whitespace-nowrap">
{t("default_calendar_selected")} {defaultCalendarSelectedString}
</span>
)
}
options={options}
styles={{
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "6px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
width: "100%",
},
};
},
}}
isSearchable={false}
className={classNames(
"block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
!hidePlaceholder && "font-medium"
)}
onChange={(option) => {
setSelectedOption(option);
if (!option) {
return;
}
/* Split only the first `:`, since Apple uses the full URL as externalId */
const [integration, externalId] = option.value.split(/:(.+)/);
onChange({
integration,
externalId,
});
}}
isLoading={isLoading}
value={selectedOption}
components={{
IndicatorSeparator: () => null,
}}
/>
</div>
);
};
export default DestinationCalendarSelector;
@@ -8,17 +8,20 @@ import { FiLink } from "@calcom/ui/components/icon";
import type { Props } from "./pages/AvailabilityPage"; import type { Props } from "./pages/AvailabilityPage";
const excludeNullValues = (value: unknown) => !!value;
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) { export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
const { t } = useLocale(); const { t } = useLocale();
return locations.length ? ( const renderLocations = locations.map((location, index) => {
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
{locations.map((location, index) => {
const eventLocationType = getEventLocationType(location.type); const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) { if (!eventLocationType) {
// It's possible that the location app got uninstalled // It's possible that the location app got uninstalled
return null; return null;
} }
if (eventLocationType.variable === "hostDefault") {
return null;
}
const translateAbleKeys = [ const translateAbleKeys = [
"attendee_in_person", "attendee_in_person",
@@ -29,7 +32,6 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
]; ];
const locationKey = z.string().default("").parse(locationKeyToString(location)); const locationKey = z.string().default("").parse(locationKeyToString(location));
const translatedLocation = location.type.startsWith("integrations:") const translatedLocation = location.type.startsWith("integrations:")
? eventLocationType.label ? eventLocationType.label
: translateAbleKeys.includes(locationKey) : translateAbleKeys.includes(locationKey)
@@ -55,7 +57,12 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
</Tooltip> </Tooltip>
</div> </div>
); );
})} });
const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[];
return filteredLocations.length ? (
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
{filteredLocations}
</div> </div>
) : null; ) : null;
} }
@@ -5,6 +5,8 @@ import { useState } from "react";
import type { EventLocationType } from "@calcom/app-store/locations"; import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType } from "@calcom/app-store/locations"; import { getEventLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
// TODO: Use browser locale, implement Intl in Dayjs maybe?
import "@calcom/dayjs/locales";
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog"; import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { formatTime } from "@calcom/lib/date-fns"; import { formatTime } from "@calcom/lib/date-fns";
@@ -77,7 +77,7 @@ export default function CancelBooking(props: Props) {
{t("nevermind")} {t("nevermind")}
</Button> </Button>
<Button <Button
className="flex justify-center" className="dark:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkmodebrandcontrast bg-brand hover:border-brand text-brandcontrast flex justify-center"
data-testid="cancel" data-testid="cancel"
onClick={async () => { onClick={async () => {
setLoading(true); setLoading(true);
+1 -4
View File
@@ -133,10 +133,7 @@ export const SlotPicker = ({
eventTypeId: eventType.id, eventTypeId: eventType.id,
eventTypeSlug: eventType.slug, eventTypeSlug: eventType.slug,
usernameList: users, usernameList: users,
startTime: startTime: dayjs().startOf("day"),
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
: browsingDate?.startOf("month"),
endTime: browsingDate?.endOf("month"), endTime: browsingDate?.endOf("month"),
timeZone, timeZone,
duration, duration,
+5 -53
View File
@@ -1,12 +1,9 @@
import type { FC } from "react"; import type { FC } from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useTheme from "@calcom/lib/hooks/useTheme";
import type { ITimezoneOption } from "@calcom/ui"; import type { ITimezoneOption } from "@calcom/ui";
import { TimezoneSelect } from "@calcom/ui"; import { TimezoneSelect } from "@calcom/ui";
import useMeQuery from "@lib/hooks/useMeQuery";
import { timeZone } from "../../lib/clock"; import { timeZone } from "../../lib/clock";
type Props = { type Props = {
@@ -15,8 +12,6 @@ type Props = {
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => { const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
const [selectedTimeZone, setSelectedTimeZone] = useState(""); const [selectedTimeZone, setSelectedTimeZone] = useState("");
const query = useMeQuery();
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
useEffect(() => { useEffect(() => {
setSelectedTimeZone(timeZone()); setSelectedTimeZone(timeZone());
@@ -28,59 +23,16 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
} }
}, [selectedTimeZone, onSelectTimeZone]); }, [selectedTimeZone, onSelectTimeZone]);
const customStyles = {
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
padding: "10px 12px !important",
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
...(userTheme === "dark" && {
color: state.isSelected || state.isFocused ? "white" : "#80868B",
}),
}),
control: () => ({
display: "flex",
cursor: "pointer",
backgroundColor: "transparent !important",
minWidth: "5rem",
height: "24px",
minHeight: "24px !important",
boxShadow: "none !important",
}),
singleValue: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
...provided,
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
...(userTheme === "dark" && {
color: "#a5a5a5 !important",
}),
}),
menu: (provided: object) => ({
...provided,
minWidth: "20rem",
margin: "8px 0 0 -20px",
}),
valueContainer: (provided: object) => ({
...provided,
padding: "0 0 0 6px",
}),
dropdownIndicator: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
...provided,
transform: state.selectProps.menuIsOpen ? "rotate(180deg)" : "",
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
marginLeft: 4,
padding: 0,
...(userTheme === "dark" && {
color: "#80868B",
}),
}),
};
return !!selectedTimeZone ? ( return !!selectedTimeZone ? (
<TimezoneSelect <TimezoneSelect
id="timeZone" id="timeZone"
classNames={{
singleValue: () => "dark:text-darkgray-600 text-gray-600",
menu: () => "!w-64 max-w-[90vw]",
}}
variant="minimal"
value={selectedTimeZone} value={selectedTimeZone}
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)} onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
className="flex h-6 text-sm font-medium"
styles={customStyles}
/> />
) : null; ) : null;
}; };
@@ -17,8 +17,8 @@ export function TimezoneDropdown({
return ( 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] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white"> <div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 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]" /> <FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
<TimeOptions onSelectTimeZone={handleSelectTimeZone} /> <TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</div> </div>
</> </>
@@ -205,6 +205,7 @@ const routerQuerySchema = z
const BookingPage = ({ const BookingPage = ({
eventType, eventType,
booking, booking,
currentSlotBooking,
profile, profile,
isDynamicGroupBooking, isDynamicGroupBooking,
recurringEventCount, recurringEventCount,
@@ -287,6 +288,7 @@ const BookingPage = ({
return router.push({ return router.push({
pathname: `/booking/${uid}`, pathname: `/booking/${uid}`,
query: { query: {
isSuccessBookingPage: true,
allRemainingBookings: true, allRemainingBookings: true,
email: bookingForm.getValues("responses.email"), email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug, eventTypeSlug: eventType.slug,
@@ -583,23 +585,27 @@ const BookingPage = ({
<div className="text-bookinghighlight flex items-start text-sm"> <div className="text-bookinghighlight flex items-start text-sm">
<FiUser <FiUser
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${ className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5 currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600" ? "text-rose-600"
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33 : currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500" ? "text-yellow-500"
: "text-bookinghighlight" : "text-bookinghighlight"
}`} }`}
/> />
<p <p
className={`${ className={`${
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5 currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600" ? "text-rose-600"
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33 : currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500" ? "text-yellow-500"
: "text-bookinghighlight" : "text-bookinghighlight"
} mb-2 font-medium`}> } mb-2 font-medium`}>
{booking {currentSlotBooking
? eventType.seatsPerTimeSlot - booking.attendees.length ? eventType.seatsPerTimeSlot - currentSlotBooking.attendees.length
: eventType.seatsPerTimeSlot}{" "} : eventType.seatsPerTimeSlot}{" "}
/ {eventType.seatsPerTimeSlot} {t("seats_available")} / {eventType.seatsPerTimeSlot} {t("seats_available")}
</p> </p>
@@ -631,6 +637,7 @@ const BookingPage = ({
</Button> </Button>
<Button <Button
type="submit" type="submit"
className="dark:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkmodebrandcontrast bg-brand hover:border-brand text-brandcontrast"
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"} data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
loading={mutation.isLoading || recurringMutation.isLoading}> loading={mutation.isLoading || recurringMutation.isLoading}>
{rescheduleUid ? t("reschedule") : t("confirm")} {rescheduleUid ? t("reschedule") : t("confirm")}
@@ -33,6 +33,7 @@ interface ISetLocationDialog {
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void; saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
selection?: LocationOption; selection?: LocationOption;
booking?: BookingItem; booking?: BookingItem;
isTeamEvent?: boolean;
defaultValues?: LocationObject[]; defaultValues?: LocationObject[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>; setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean; isOpenDialog: boolean;
@@ -74,6 +75,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
saveLocation, saveLocation,
selection, selection,
booking, booking,
isTeamEvent,
setShowLocationModal, setShowLocationModal,
isOpenDialog, isOpenDialog,
defaultValues, defaultValues,
@@ -290,7 +292,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
query={locationsQuery} query={locationsQuery}
success={({ data }) => { success={({ data }) => {
if (!data.length) return null; if (!data.length) return null;
const locationOptions = [...data]; const locationOptions = [...data].filter((option) => {
return !isTeamEvent ? option.label !== "Conferencing" : true;
});
if (booking) { if (booking) {
locationOptions.map((location) => locationOptions.map((location) =>
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value)) location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
@@ -4,13 +4,15 @@ import type { OptionProps, SingleValueProps } from "react-select";
import { components } from "react-select"; import { components } from "react-select";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { NewScheduleButton } from "@calcom/features/schedules";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday"; import { weekdayNames } from "@calcom/lib/weekday";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui"; import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon"; import { FiExternalLink, FiGlobe, FiClock } from "@calcom/ui/components/icon";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader"; import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
@@ -52,23 +54,24 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
const AvailabilitySelect = ({ const AvailabilitySelect = ({
className = "", className = "",
isLoading,
schedules,
...props ...props
}: { }: {
className?: string; className?: string;
name: string; name: string;
value: number; value: number;
isLoading: boolean;
schedules: RouterOutputs["viewer"]["availability"]["list"]["schedules"] | [];
onBlur: () => void; onBlur: () => void;
onChange: (value: AvailabilityOption | null) => void; onChange: (value: AvailabilityOption | null) => void;
}) => { }) => {
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
const { t } = useLocale(); const { t } = useLocale();
if (isLoading) { if (isLoading) {
return <SelectSkeletonLoader />; return <SelectSkeletonLoader />;
} }
const schedules = data?.schedules || [];
const options = schedules.map((schedule) => ({ const options = schedules.map((schedule) => ({
value: schedule.id, value: schedule.id,
label: schedule.name, label: schedule.name,
@@ -107,6 +110,7 @@ const EventTypeScheduleDetails = () => {
const { watch } = useFormContext<FormValues>(); const { watch } = useFormContext<FormValues>();
const scheduleId = watch("schedule"); const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery( const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined }, { scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId } { enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
@@ -174,6 +178,19 @@ const EventTypeScheduleDetails = () => {
const EventTypeSchedule = () => { const EventTypeSchedule = () => {
const { t } = useLocale(); const { t } = useLocale();
const { data: schedules, isLoading } = trpc.viewer.availability.list.useQuery();
if (!schedules?.schedules.length && !isLoading)
return (
<EmptyScreen
Icon={FiClock}
headline={t("new_schedule_heading")}
description={t("new_schedule_description")}
buttonRaw={<NewScheduleButton fromEventType />}
border={false}
/>
);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
@@ -183,14 +200,18 @@ const EventTypeSchedule = () => {
<Controller <Controller
name="schedule" name="schedule"
render={({ field }) => ( render={({ field }) => (
<>
<AvailabilitySelect <AvailabilitySelect
value={field.value} value={field.value}
onBlur={field.onBlur} onBlur={field.onBlur}
name={field.name} name={field.name}
schedules={schedules?.schedules || []}
isLoading={isLoading}
onChange={(selected) => { onChange={(selected) => {
field.onChange(selected?.value || null); field.onChange(selected?.value || null);
}} }}
/> />
</>
)} )}
/> />
</div> </div>
@@ -88,6 +88,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
label={t("minimum_booking_notice")} label={t("minimum_booking_notice")}
type="number" type="number"
placeholder="0" placeholder="0"
min={0}
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2" className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
/> />
<input type="hidden" ref={ref} {...passThroughProps} /> <input type="hidden" ref={ref} {...passThroughProps} />
@@ -425,6 +426,7 @@ const IntervalLimitItem = ({
required required
type="number" type="number"
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"} containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
className="mb-0"
placeholder={`${value}`} placeholder={`${value}`}
min={step} min={step}
step={step} step={step}
+43 -16
View File
@@ -4,7 +4,7 @@ import { isValidPhoneNumber } from "libphonenumber-js";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import Link from "next/link"; import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form"; import { Controller, useForm, useFormContext } from "react-hook-form";
import type { MultiValue } from "react-select"; import type { MultiValue } from "react-select";
import { z } from "zod"; import { z } from "zod";
@@ -16,7 +16,17 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt"; import { md } from "@calcom/lib/markdownIt";
import { slugify } from "@calcom/lib/slugify"; import { slugify } from "@calcom/lib/slugify";
import turndown from "@calcom/lib/turndownService"; import turndown from "@calcom/lib/turndownService";
import { Button, Editor, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui"; import {
Button,
Label,
Select,
SettingsToggle,
Skeleton,
TextField,
Editor,
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon"; import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
@@ -35,14 +45,31 @@ const getLocationFromType = (
} }
}; };
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => { interface DescriptionEditorProps {
for (const locationType of options) { description?: string | null;
for (const location of locationType.options) {
if (location.value === type && location.disabled === false) {
return location;
}
}
} }
const DescriptionEditor = (props: DescriptionEditorProps) => {
const formMethods = useFormContext<FormValues>();
const [mounted, setIsMounted] = useState(false);
const { t } = useLocale();
const { description } = props;
useEffect(() => {
setIsMounted(true);
}, []);
return mounted ? (
<Editor
getText={() => md.render(formMethods.getValues("description") || description || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
/>
) : (
<SkeletonContainer>
<SkeletonText className="block h-24 w-full" />
</SkeletonContainer>
);
}; };
export const EventSetupTab = ( export const EventSetupTab = (
@@ -53,12 +80,16 @@ export const EventSetupTab = (
) => { ) => {
const { t } = useLocale(); const { t } = useLocale();
const formMethods = useFormContext<FormValues>(); const formMethods = useFormContext<FormValues>();
const { eventType, locationOptions, team, destinationCalendar } = props; const { eventType, team, destinationCalendar } = props;
const [showLocationModal, setShowLocationModal] = useState(false); const [showLocationModal, setShowLocationModal] = useState(false);
const [editingLocationType, setEditingLocationType] = useState<string>(""); const [editingLocationType, setEditingLocationType] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined); const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration); const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
const locationOptions = props.locationOptions.filter((option) => {
return !team ? option.label !== "Conferencing" : true;
});
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({ const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
value: mins, value: mins,
label: t("multiple_duration_mins", { count: mins }), label: t("multiple_duration_mins", { count: mins }),
@@ -287,12 +318,7 @@ export const EventSetupTab = (
/> />
<div> <div>
<Label>{t("description")}</Label> <Label>{t("description")}</Label>
<Editor <DescriptionEditor description={eventType?.description} />
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
/>
</div> </div>
<TextField <TextField
required required
@@ -409,6 +435,7 @@ export const EventSetupTab = (
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */} {/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
<EditLocationDialog <EditLocationDialog
isTeamEvent={!!team}
isOpenDialog={showLocationModal} isOpenDialog={showLocationModal}
setShowLocationModal={setShowLocationModal} setShowLocationModal={setShowLocationModal}
saveLocation={saveLocation} saveLocation={saveLocation}
@@ -1,11 +1,11 @@
export type EventTypeDescriptionSafeProps = { export type EventTypeDescriptionSafeProps = {
eventType: { description: string | null }; eventType: { description: string | null; descriptionAsSafeHTML: string | null };
}; };
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => { export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true }; const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
// @ts-expect-error: @see packages/prisma/middleware/eventTypeDescriptionParseAndSanitize.ts if (eventType.description)
if (eventType.description) props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML }; props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML || "" };
return <div {...props} />; return <div {...props} />;
}; };
@@ -247,7 +247,7 @@ function EventTypeSingleLayout({
tooltip={t("embed")} tooltip={t("embed")}
/> />
<Button <Button
color="secondary" color="destructive"
variant="icon" variant="icon"
StartIcon={FiTrash} StartIcon={FiTrash}
tooltip={t("delete")} tooltip={t("delete")}
@@ -1,9 +1,8 @@
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterInputs } from "@calcom/trpc/react"; import type { RouterInputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
interface ICreateEventsOnCalendarSelectProps { interface ICreateEventsOnCalendarSelectProps {
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null; calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
} }
@@ -75,7 +75,9 @@ export function CalendarSwitch(props: {
/> />
{props.defaultSelected && ( {props.defaultSelected && (
<Badge variant="gray"> <Badge variant="gray">
<div className="flex">
<FiArrowLeft className="mr-1" /> {t("adding_events_to")} <FiArrowLeft className="mr-1" /> {t("adding_events_to")}
</div>
</Badge> </Badge>
)} )}
</div> </div>
+6 -5
View File
@@ -2,13 +2,14 @@ import Link from "next/link";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams"; import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { Avatar } from "@calcom/ui"; import { Avatar } from "@calcom/ui";
type TeamType = NonNullable<TeamWithMembers>; type TeamType = NonNullable<TeamWithMembers>;
type MembersType = TeamType["members"]; type MembersType = TeamType["members"];
type MemberType = MembersType[number]; type MemberType = MembersType[number] & { safeBio: string | null };
type TeamTypeWithSafeHtml = Omit<TeamType, "members"> & { members: MemberType[] };
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => { const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
const { t } = useLocale(); const { t } = useLocale();
@@ -30,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
<> <>
<div <div
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600" className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }} dangerouslySetInnerHTML={{ __html: member.safeBio || "" }}
/> />
</> </>
) : ( ) : (
@@ -43,7 +44,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
); );
}; };
const Members = ({ members, teamName }: { members: MembersType; teamName: string | null }) => { const Members = ({ members, teamName }: { members: MemberType[]; teamName: string | null }) => {
if (!members || members.length === 0) { if (!members || members.length === 0) {
return null; return null;
} }
@@ -57,7 +58,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
); );
}; };
const Team = ({ team }: { team: TeamType }) => { const Team = ({ team }: { team: TeamTypeWithSafeHtml }) => {
return ( return (
<div> <div>
<Members members={team.members} teamName={team.name} /> <Members members={team.members} teamName={team.name} />
+9
View File
@@ -10,6 +10,8 @@ import type { ComponentProps, ReactNode } from "react";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic"; import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic"; import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { FeatureProvider } from "@calcom/features/flags/context/provider";
import { useFlags } from "@calcom/features/flags/hooks";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { MetaProvider } from "@calcom/ui"; import { MetaProvider } from "@calcom/ui";
@@ -58,6 +60,11 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
return <I18nextAdapter {...passedProps} />; return <I18nextAdapter {...passedProps} />;
}; };
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
const flags = useFlags();
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
}
const AppProviders = (props: AppPropsWithChildren) => { const AppProviders = (props: AppPropsWithChildren) => {
const session = trpc.viewer.public.session.useQuery().data; const session = trpc.viewer.public.session.useQuery().data;
// No need to have intercom on public pages - Good for Page Performance // No need to have intercom on public pages - Good for Page Performance
@@ -85,7 +92,9 @@ const AppProviders = (props: AppPropsWithChildren) => {
storageKey={storageKey} storageKey={storageKey}
forcedTheme={forcedTheme} forcedTheme={forcedTheme}
attribute="class"> attribute="class">
<FeatureFlagsProvider>
<MetaProvider>{props.children}</MetaProvider> <MetaProvider>{props.children}</MetaProvider>
</FeatureFlagsProvider>
</ThemeProvider> </ThemeProvider>
</TooltipProvider> </TooltipProvider>
</CustomI18nextProvider> </CustomI18nextProvider>
+6 -22
View File
@@ -1,9 +1,8 @@
import type { Prisma, PrismaClient } from "@prisma/client"; import type { Prisma, PrismaClient } from "@prisma/client";
import type { z } from "zod"; import type { z } from "zod";
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import slugify from "@calcom/lib/slugify"; import slugify from "@calcom/lib/slugify";
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
type BookingSelect = { type BookingSelect = {
description: true; description: true;
@@ -45,11 +44,7 @@ function getResponsesFromOldBooking(
}; };
} }
async function getBooking( async function getBooking(prisma: PrismaClient, uid: string) {
prisma: PrismaClient,
uid: string,
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">
) {
const rawBooking = await prisma.booking.findFirst({ const rawBooking = await prisma.booking.findFirst({
where: { where: {
uid, uid,
@@ -82,9 +77,7 @@ async function getBooking(
return rawBooking; return rawBooking;
} }
const booking = getBookingWithResponses(rawBooking, { const booking = getBookingWithResponses(rawBooking);
bookingFields,
});
if (booking) { if (booking) {
// @NOTE: had to do this because Server side cant return [Object objects] // @NOTE: had to do this because Server side cant return [Object objects]
@@ -104,20 +97,11 @@ export const getBookingWithResponses = <
}; };
}> }>
>( >(
booking: T, booking: T
eventType: {
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
}
) => { ) => {
return { return {
...booking, ...booking,
responses: getBookingResponsesPartialSchema({ responses: bookingResponsesDbSchema.parse(booking.responses || getResponsesFromOldBooking(booking)),
eventType: { } as Omit<T, "responses"> & { responses: z.infer<typeof bookingResponsesDbSchema> };
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)),
};
}; };
export default getBooking; export default getBooking;
+8
View File
@@ -99,6 +99,14 @@ const nextConfig = {
skipDefaultConversion: true, skipDefaultConversion: true,
preventFullImport: true, preventFullImport: true,
}, },
"@calcom/features/insights/components": {
transform: "@calcom/features/insights/components/{{member}}",
skipDefaultConversion: true,
preventFullImport: true,
},
lodash: {
transform: "lodash/{{member}}",
},
// TODO: We need to have all components in `@calcom/ui/components` in order to use this // TODO: We need to have all components in `@calcom/ui/components` in order to use this
// "@calcom/ui": { // "@calcom/ui": {
// transform: "@calcom/ui/components/{{member}}", // transform: "@calcom/ui/components/{{member}}",
+6 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "2.7.3", "version": "2.7.7",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
@@ -11,6 +11,7 @@
"dx": "yarn dev", "dx": "yarn dev",
"test-codegen": "yarn playwright codegen http://localhost:3000", "test-codegen": "yarn playwright codegen http://localhost:3000",
"type-check": "tsc --pretty --noEmit", "type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint . --ignore-path .gitignore", "lint": "eslint . --ignore-path .gitignore",
@@ -60,10 +61,11 @@
"@stripe/react-stripe-js": "^1.10.0", "@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0", "@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9", "@tanstack/react-query": "^4.3.9",
"@tremor/react": "^2.0.0",
"@types/turndown": "^5.0.1", "@types/turndown": "^5.0.1",
"@vercel/edge-config": "^0.1.1", "@vercel/edge-config": "^0.1.1",
"@vercel/edge-functions-ui": "^0.2.1", "@vercel/edge-functions-ui": "^0.2.1",
"@vercel/og": "^0.0.21", "@vercel/og": "^0.5.0",
"accept-language-parser": "^1.5.0", "accept-language-parser": "^1.5.0",
"async": "^3.2.4", "async": "^3.2.4",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
@@ -118,6 +120,7 @@
"react-window": "^1.8.7", "react-window": "^1.8.7",
"remark": "^14.0.2", "remark": "^14.0.2",
"rrule": "^2.7.1", "rrule": "^2.7.1",
"sanitize-html": "^2.10.0",
"schema-dts": "^1.1.0", "schema-dts": "^1.1.0",
"short-uuid": "^4.2.0", "short-uuid": "^4.2.0",
"strip-markdown": "^5.0.0", "strip-markdown": "^5.0.0",
@@ -155,6 +158,7 @@
"@types/react-phone-number-input": "^3.0.14", "@types/react-phone-number-input": "^3.0.14",
"@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5", "@types/react-window": "^1.8.5",
"@types/sanitize-html": "^2.9.0",
"@types/stripe": "^8.0.417", "@types/stripe": "^8.0.417",
"@types/uuid": "8.3.1", "@types/uuid": "8.3.1",
"autoprefixer": "^10.4.12", "autoprefixer": "^10.4.12",
+38 -1
View File
@@ -6,7 +6,7 @@ import { useEffect, useState } from "react";
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants"; import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui"; import { HeadSeo } from "@calcom/ui";
import { FiFileText, FiCheck, FiBookOpen, FiChevronRight } from "@calcom/ui/components/icon"; import { FiBookOpen, FiCheck, FiChevronRight, FiFileText } from "@calcom/ui/components/icon";
import { ssgInit } from "@server/lib/ssg"; import { ssgInit } from "@server/lib/ssg";
@@ -40,6 +40,43 @@ export default function Custom404() {
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage; const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
const isSignup = router.asPath.startsWith("/signup"); const isSignup = router.asPath.startsWith("/signup");
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com"; const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
/**
* If we're on 404 and the route is insights it means it is disabled
* TODO: Abstract this for all disabled features
**/
const isInsights = router.asPath.startsWith("/insights");
if (isInsights) {
return (
<>
<HeadSeo
title="Feature is currently disabled"
description={t("404_page_not_found")}
nextSeoProps={{
nofollow: true,
noindex: true,
}}
/>
<div className="min-h-screen bg-white px-4" data-testid="404-page">
<main className="mx-auto max-w-xl pt-16 pb-6 sm:pt-24">
<div className="text-center">
<p className="text-sm font-semibold uppercase tracking-wide text-black">{t("error_404")}</p>
<h1 className="font-cal mt-2 text-4xl font-extrabold text-gray-900 sm:text-5xl">
Feature is currently disabled
</h1>
</div>
<div className="mt-12">
<div className="mt-8">
<Link href="/" className="text-base font-medium text-black hover:text-gray-500">
{t("or_go_back_home")}
<span aria-hidden="true"> &rarr;</span>
</Link>
</div>
</div>
</main>
</div>
</>
);
}
return ( return (
<> <>
+6 -2
View File
@@ -24,7 +24,7 @@ import defaultEvents, {
} from "@calcom/lib/defaultEvents"; } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
import { md } from "@calcom/lib/markdownIt"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects"; import { baseEventTypeSelect } from "@calcom/prisma/selects";
@@ -147,7 +147,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<> <>
<div <div
className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600" className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(user.bio || "") }} dangerouslySetInnerHTML={{ __html: props.safeBio }}
/> />
</> </>
)} )}
@@ -343,6 +343,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const eventTypes = eventTypesRaw.map((eventType) => ({ const eventTypes = eventTypesRaw.map((eventType) => ({
...eventType, ...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}), metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
})); }));
const isSingleUser = users.length === 1; const isSingleUser = users.length === 1;
@@ -352,9 +353,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}) })
: []; : [];
const safeBio = markdownToSafeHTML(user.bio) || "";
return { return {
props: { props: {
users, users,
safeBio,
profile, profile,
user: { user: {
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"), emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
+2 -6
View File
@@ -5,7 +5,7 @@ import type { LocationObject } from "@calcom/app-store/locations";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata"; import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { addListFormatting } from "@calcom/lib/markdownIt"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { User } from "@calcom/prisma/client"; import type { User } from "@calcom/prisma/client";
import { isBrandingHidden } from "@lib/isBrandingHidden"; import { isBrandingHidden } from "@lib/isBrandingHidden";
@@ -59,7 +59,6 @@ Type.isThemeSupported = true;
const paramsSchema = z.object({ type: z.string(), user: z.string() }); const paramsSchema = z.object({ type: z.string(), user: z.string() });
async function getUserPageProps(context: GetStaticPropsContext) { async function getUserPageProps(context: GetStaticPropsContext) {
// load server side dependencies // load server side dependencies
const MarkdownIt = await import("markdown-it").then((mod) => mod.default);
const prisma = await import("@calcom/prisma").then((mod) => mod.default); const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const { privacyFilteredLocations } = await import("@calcom/app-store/locations"); const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent"); const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
@@ -124,9 +123,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
}, },
}, },
}); });
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
if (!user || !user.eventTypes.length) return { notFound: true }; if (!user || !user.eventTypes.length) return { notFound: true };
const [eventType]: ((typeof user.eventTypes)[number] & { const [eventType]: ((typeof user.eventTypes)[number] & {
@@ -153,7 +149,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}), metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
recurringEvent: parseRecurringEvent(eventType.recurringEvent), recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: privacyFilteredLocations(locations), locations: privacyFilteredLocations(locations),
descriptionAsSafeHTML: eventType.description ? addListFormatting(md.render(eventType.description)) : null, descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}); });
// Check if the user you are logging into has any active teams or premium user name // Check if the user you are logging into has any active teams or premium user name
const hasActiveTeam = const hasActiveTeam =
+9 -6
View File
@@ -14,6 +14,7 @@ import {
getUsernameList, getUsernameList,
} from "@calcom/lib/defaultEvents"; } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma, { bookEventTypeSelect } from "@calcom/prisma"; import prisma, { bookEventTypeSelect } from "@calcom/prisma";
import { import {
customInputSchema, customInputSchema,
@@ -189,6 +190,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
slug: u.username, slug: u.username,
theme: u.theme, theme: u.theme,
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}; };
})[0]; })[0];
@@ -217,7 +219,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
rescheduleUid = bookingSeat.booking.uid; rescheduleUid = bookingSeat.booking.uid;
attendeeEmail = bookingSeat.attendee.email; attendeeEmail = bookingSeat.attendee.email;
} }
} else if (eventTypeRaw.seatsPerTimeSlot && query.duration && query.date) { }
if (query.duration) {
// If it's not reschedule but event Type has seats we should obtain // If it's not reschedule but event Type has seats we should obtain
// the bookingUid regardless and use it to get the booking // the bookingUid regardless and use it to get the booking
const currentSeats = await prisma.booking.findFirst({ const currentSeats = await prisma.booking.findFirst({
@@ -237,11 +241,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
let booking: GetBookingType | null = null; let booking: GetBookingType | null = null;
if (rescheduleUid || query.bookingUid || bookingUidWithSeats) { if (rescheduleUid || query.bookingUid || bookingUidWithSeats) {
booking = await getBooking( booking = await getBooking(prisma, rescheduleUid || query.bookingUid || bookingUidWithSeats || "");
prisma,
rescheduleUid || query.bookingUid || bookingUidWithSeats || "",
eventTypeObject.bookingFields
);
} }
if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) { if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) {
@@ -285,12 +285,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
: eventType.recurringEvent.count)) || : eventType.recurringEvent.count)) ||
null; null;
const currentSlotBooking = await getBooking(prisma, bookingUidWithSeats || "");
return { return {
props: { props: {
away: user.away, away: user.away,
profile, profile,
eventType: eventTypeObject, eventType: eventTypeObject,
booking, booking,
currentSlotBooking: currentSlotBooking,
recurringEventCount, recurringEventCount,
trpcState: ssr.dehydrate(), trpcState: ssr.dehydrate(),
isDynamicGroupBooking, isDynamicGroupBooking,
+37 -4
View File
@@ -1,4 +1,12 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments";
import prisma from "@calcom/prisma";
const querySchema = z.object({
page: z.coerce.number().min(0).optional().default(0),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey; const apiKey = req.headers.authorization || req.query.apiKey;
@@ -11,10 +19,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return; return;
} }
/** const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
* TODO: const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
* Remove this endpoint let { page: pageNumber } = querySchema.parse(req.query);
*/
while (true) {
const teams = await prisma.team.findMany({
where: {
slug: {
not: null,
},
},
select: {
id: true,
},
skip: pageNumber * pageSize,
take: pageSize,
});
if (teams.length === 0) {
break;
}
for (const team of teams) {
await updateQuantitySubscriptionFromStripe(team.id);
await delay(100); // Adjust the delay as needed to avoid rate limiting
}
pageNumber++;
}
res.json({ ok: true }); res.json({ ok: true });
} }
+3 -3
View File
@@ -88,7 +88,7 @@ export default async function handler(req: NextApiRequest) {
ogConfig ogConfig
) as { body: Buffer }; ) as { body: Buffer };
return new Response(img.body, { status: 200 }); return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
} }
case "app": { case "app": {
const { name, description, slug } = appSchema.parse({ const { name, description, slug } = appSchema.parse({
@@ -101,7 +101,7 @@ export default async function handler(req: NextApiRequest) {
body: Buffer; body: Buffer;
}; };
return new Response(img.body, { status: 200 }); return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
} }
case "generic": { case "generic": {
@@ -115,7 +115,7 @@ export default async function handler(req: NextApiRequest) {
body: Buffer; body: Buffer;
}; };
return new Response(img.body, { status: 200 }); return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
} }
default: default:
+3 -3
View File
@@ -14,7 +14,7 @@ import { FiSearch } from "@calcom/ui/components/icon";
import AppsLayout from "@components/apps/layouts/AppsLayout"; import AppsLayout from "@components/apps/layouts/AppsLayout";
import { ssgInit } from "@server/lib/ssg"; import { ssrInit } from "@server/lib/ssr";
const tabs: HorizontalTabItemProps[] = [ const tabs: HorizontalTabItemProps[] = [
{ {
@@ -88,7 +88,7 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
export const getServerSideProps = async (context: GetServerSidePropsContext) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context; const { req, res } = context;
const ssg = await ssgInit(context); const ssr = await ssrInit(context);
const session = await getServerSession({ req, res }); const session = await getServerSession({ req, res });
@@ -119,7 +119,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return b.count - a.count; return b.count - a.count;
}), }),
appStore, appStore,
trpcState: ssg.dehydrate(), trpcState: ssr.dehydrate(),
}, },
}; };
}; };
+1 -1
View File
@@ -1,5 +1,5 @@
import type { ResetPasswordRequest } from "@prisma/client"; import type { ResetPasswordRequest } from "@prisma/client";
import debounce from "lodash/debounce"; import { debounce } from "lodash";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react"; import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { serverSideTranslations } from "next-i18next/serverSideTranslations";
@@ -1,4 +1,4 @@
import debounce from "lodash/debounce"; import { debounce } from "lodash";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react"; import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+10 -2
View File
@@ -94,6 +94,7 @@ export default function Availability() {
data: { schedule: scheduleId }, data: { schedule: scheduleId },
} = useTypedQuery(querySchema); } = useTypedQuery(querySchema);
const { fromEventType } = router.query;
const { timeFormat } = me.data || { timeFormat: null }; const { timeFormat } = me.data || { timeFormat: null };
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery( const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId }, { scheduleId },
@@ -153,7 +154,7 @@ export default function Availability() {
return ( return (
<Shell <Shell
backPath="/availability" backPath={fromEventType ? true : "/availability"}
title={schedule?.name ? schedule.name + " | " + t("availability") : t("availability")} title={schedule?.name ? schedule.name + " | " + t("availability") : t("availability")}
heading={ heading={
<Controller <Controller
@@ -200,7 +201,14 @@ export default function Availability() {
<VerticalDivider /> <VerticalDivider />
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button StartIcon={FiTrash} variant="icon" color="destructive" aria-label={t("delete")} /> <Button
StartIcon={FiTrash}
variant="icon"
color="destructive"
aria-label={t("delete")}
disabled={schedule?.isLastSchedule}
tooltip={t("requires_at_least_one_schedule")}
/>
</DialogTrigger> </DialogTrigger>
<ConfirmationDialogContent <ConfirmationDialogContent
isLoading={deleteMutation.isLoading} isLoading={deleteMutation.isLoading}
+7 -1
View File
@@ -93,7 +93,13 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
key={schedule.id} key={schedule.id}
schedule={schedule} schedule={schedule}
updateDefault={updateMutation.mutate} updateDefault={updateMutation.mutate}
deleteFunction={deleteMutation.mutate} deleteFunction={() => {
if (schedules.length === 1) {
showToast(t("requires_at_least_one_schedule"), "error");
} else {
deleteMutation.mutate;
}
}}
/> />
))} ))}
</ul> </ul>
+13 -14
View File
@@ -180,7 +180,7 @@ const querySchema = z.object({
cancel: stringToBoolean, cancel: stringToBoolean,
changes: stringToBoolean, changes: stringToBoolean,
reschedule: stringToBoolean, reschedule: stringToBoolean,
isSuccessBookingPage: z.string().optional(), isSuccessBookingPage: stringToBoolean,
formerTime: z.string().optional(), formerTime: z.string().optional(),
email: z.string().optional(), email: z.string().optional(),
seatReferenceUid: z.string().optional(), seatReferenceUid: z.string().optional(),
@@ -266,7 +266,11 @@ export default function Success(props: SuccessProps) {
const giphyImage = giphyAppData?.thankYouPage; const giphyImage = giphyAppData?.thankYouPage;
const eventName = getEventName(eventNameObject, true); const eventName = getEventName(eventNameObject, true);
const needsConfirmation = eventType.requiresConfirmation && reschedule != true; // Confirmation can be needed in two cases as of now
// - Event Type has require confirmation option enabled always
// - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
// - It's a paid event and payment is pending.
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING;
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id); const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
const isCancelled = const isCancelled =
@@ -485,7 +489,7 @@ export default function Success(props: SuccessProps) {
</> </>
)} )}
<div className="font-medium">{t("what")}</div> <div className="font-medium">{t("what")}</div>
<div className="col-span-2 mb-6 last:mb-0">{eventName}</div> <div className="col-span-2 mb-6 last:mb-0">{props.bookingInfo.title}</div>
<div className="font-medium">{t("when")}</div> <div className="font-medium">{t("when")}</div>
<div className="col-span-2 mb-6 last:mb-0"> <div className="col-span-2 mb-6 last:mb-0">
{reschedule && !!formerTime && ( {reschedule && !!formerTime && (
@@ -527,7 +531,7 @@ export default function Success(props: SuccessProps) {
</div> </div>
)} )}
{bookingInfo?.attendees.map((attendee) => ( {bookingInfo?.attendees.map((attendee) => (
<div key={attendee.name} className="mb-3 last:mb-0"> <div key={attendee.name + attendee.email} className="mb-3 last:mb-0">
{attendee.name && <p>{attendee.name}</p>} {attendee.name && <p>{attendee.name}</p>}
<p data-testid={`attendee-${attendee.email}`} className="text-bookinglight"> <p data-testid={`attendee-${attendee.email}`} className="text-bookinglight">
{attendee.email} {attendee.email}
@@ -565,7 +569,8 @@ export default function Success(props: SuccessProps) {
</div> </div>
</> </>
)} )}
</div>
<div className="text-bookingdark dark:border-darkgray-200 mt-8 text-left dark:text-gray-300">
{Object.entries(bookingInfo.responses).map(([name, response]) => { {Object.entries(bookingInfo.responses).map(([name, response]) => {
const field = eventType.bookingFields.find((field) => field.name === name); const field = eventType.bookingFields.find((field) => field.name === name);
// We show location in the "where" section // We show location in the "where" section
@@ -580,15 +585,10 @@ export default function Success(props: SuccessProps) {
return ( return (
<> <>
<div className="mt-9 font-medium">{label}</div> <div className="mt-4 font-medium">{label}</div>
<div className="col-span-2 mb-2 mt-9"> <p className="break-words" data-testid="field-response" data-fob-field={field.name}>
<p
className="break-words"
data-testid="field-response"
data-fob-field={field.name}>
{response.toString()} {response.toString()}
</p> </p>
</div>
</> </>
); );
})} })}
@@ -1087,8 +1087,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}; };
} }
const bookingInfo = getBookingWithResponses(bookingInfoRaw, eventTypeRaw); const bookingInfo = getBookingWithResponses(bookingInfoRaw);
// @NOTE: had to do this because Server side cant return [Object objects] // @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse // probably fixable with json.stringify -> json.parse
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date; bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
+2
View File
@@ -5,6 +5,7 @@ import type { LocationObject } from "@calcom/core/location";
import { privacyFilteredLocations } from "@calcom/core/location"; import { privacyFilteredLocations } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import { getWorkingHours } from "@calcom/lib/availability"; import { getWorkingHours } from "@calcom/lib/availability";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { availiblityPageEventTypeSelect } from "@calcom/prisma"; import { availiblityPageEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -119,6 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
hideBranding: u.hideBranding, hideBranding: u.hideBranding,
timeZone: u.timeZone, timeZone: u.timeZone,
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(hashedLink.eventType.description),
}); });
const [user] = users; const [user] = users;
+3
View File
@@ -1,6 +1,7 @@
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { bookEventTypeSelect } from "@calcom/prisma/selects"; import { bookEventTypeSelect } from "@calcom/prisma/selects";
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -93,6 +94,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
brandColor: u.brandColor, brandColor: u.brandColor,
darkBrandColor: u.darkBrandColor, darkBrandColor: u.darkBrandColor,
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}; };
})[0]; })[0];
@@ -120,6 +122,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
profile, profile,
eventType: eventTypeObject, eventType: eventTypeObject,
booking: null, booking: null,
currentSlotBooking: null,
trpcState: ssr.dehydrate(), trpcState: ssr.dehydrate(),
recurringEventCount, recurringEventCount,
isDynamicGroupBooking: false, isDynamicGroupBooking: false,
+5 -19
View File
@@ -13,11 +13,9 @@ import type { EventLocationType } from "@calcom/core/location";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { validateIntervalLimitOrder } from "@calcom/lib"; import { validateIntervalLimitOrder } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import getEventTypeById from "@calcom/lib/getEventTypeById";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client"; import type { Prisma } from "@calcom/prisma/client";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -65,6 +63,7 @@ export type FormValues = {
hostPhoneNumber?: string; hostPhoneNumber?: string;
displayLocationPublicly?: boolean; displayLocationPublicly?: boolean;
phone?: string; phone?: string;
hostDefault?: string;
}[]; }[];
customInputs: CustomInputParsed[]; customInputs: CustomInputParsed[];
schedule: number | null; schedule: number | null;
@@ -365,9 +364,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
}; };
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => { const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
const { data, isLoading } = trpc.viewer.eventTypes.get.useQuery({ id: props.type }); const { data } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
if (isLoading || !data) return null;
return <EventTypePage {...data} />; return <EventTypePage {...(data as EventTypeSetupProps)} />;
}; };
export const getServerSideProps = async (context: GetServerSidePropsContext) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
@@ -393,26 +392,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}; };
} }
try { await ssr.viewer.eventTypes.get.prefetch({ id: typeParam });
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma });
return { return {
props: { props: {
// session,
type: typeParam, type: typeParam,
trpcState: ssr.dehydrate(), trpcState: ssr.dehydrate(),
initialData: {
eventType: res.eventType,
locationOptions: res.locationOptions,
team: res.team,
teamMembers: res.teamMembers,
currentUserMembership: res.currentUserMembership,
},
}, },
}; };
} catch (err) {
throw err;
}
}; };
export default EventTypePageWrapper; export default EventTypePageWrapper;
+1 -1
View File
@@ -141,7 +141,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
</div> </div>
<EventTypeDescription <EventTypeDescription
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon // @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
eventType={type} eventType={{ ...type, descriptionAsSafeHTML: type.safeDescription }}
shortenDescription shortenDescription
/> />
</Link> </Link>
+133
View File
@@ -0,0 +1,133 @@
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import {
AverageEventDurationChart,
BookingKPICards,
BookingStatusLineChart,
LeastBookedTeamMembersTable,
MostBookedTeamMembersTable,
PopularEventsTable,
} from "@calcom/features/insights/components";
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
import { useFilterContext } from "@calcom/features/insights/context/provider";
import { Filters } from "@calcom/features/insights/filters";
import Shell from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import { Button, ButtonGroup } from "@calcom/ui";
import { FiRefreshCcw, FiUserPlus, FiUsers } from "@calcom/ui/components/icon";
const Heading = () => {
const { t } = useLocale();
const {
filter: { selectedTeamName },
} = useFilterContext();
return (
<div className="min-w-52">
<h3 className="font-cal max-w-28 sm:max-w-72 md:max-w-80 hidden truncate text-xl font-semibold tracking-wide text-black md:block xl:max-w-full">
{t("analytics_for_organisation", {
organisationName: selectedTeamName,
})}
</h3>
<p className="hidden text-sm text-gray-500 md:block">{t("subtitle_analytics")}</p>
</div>
);
};
export default function InsightsPage() {
const { t } = useLocale();
const { data: user } = trpc.viewer.me.useQuery();
const features = [
{
icon: <FiUsers className="h-5 w-5" />,
title: t("view_bookings_across"),
description: t("view_bookings_across_description"),
},
{
icon: <FiRefreshCcw className="h-5 w-5" />,
title: t("identify_booking_trends"),
description: t("identify_booking_trends_description"),
},
{
icon: <FiUserPlus className="h-5 w-5" />,
title: t("spot_popular_event_types"),
description: t("spot_popular_event_types_description"),
},
];
return (
<div>
<Shell>
<UpgradeTip
title={t("make_informed_decisions")}
description={t("make_informed_decisions_description")}
features={features}
background="/tips/insights.jpg"
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("create_team")}
</Button>
<Button color="secondary" href="https://go.cal.com/insights" target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
</div>
}>
{!user ? (
<></>
) : (
<FiltersProvider>
<div className="mb-4 mt-0 ml-auto flex w-full flex-wrap justify-between md:-mt-8">
<Heading />
<Filters />
</div>
<div className="mb-4 space-y-6">
<BookingKPICards />
<BookingStatusLineChart />
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
<PopularEventsTable />
<AverageEventDurationChart />
</div>
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
<MostBookedTeamMembersTable />
<LeastBookedTeamMembersTable />
</div>
<small className="block text-center text-gray-600">
{t("looking_for_more_analytics")}
<a
className="text-blue-500 hover:underline"
href="mailto:updates@cal.com?subject=Feature%20Request%3A%20More%20Analytics&body=Hey%20Cal.com%20Team%2C%20I%20love%20the%20analytics%20page%20but%20I%20am%20looking%20for%20...">
{" "}
{t("contact_support")}
</a>
</small>
</div>
</FiltersProvider>
)}
</UpgradeTip>
</Shell>
</div>
);
}
// If feature flag is disabled, return not found on getServerSideProps
export const getServerSideProps = async () => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
if (flags.insights === false) {
return {
notFound: true,
};
}
return {
props: {},
};
};
+9
View File
@@ -0,0 +1,9 @@
import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view";
import { getLayout } from "@components/auth/layouts/AdminLayout";
const FlagsPage = () => <FlagListingView />;
FlagsPage.getLayout = getLayout;
export default FlagsPage;
@@ -1,6 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { IdentityProvider } from "@prisma/client"; import { IdentityProvider } from "@prisma/client";
import crypto from "crypto";
import { signOut } from "next-auth/react"; import { signOut } from "next-auth/react";
import type { BaseSyntheticEvent } from "react"; import type { BaseSyntheticEvent } from "react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
@@ -332,11 +331,6 @@ const ProfileForm = ({
bio: z.string(), bio: z.string(),
}); });
const emailMd5 = crypto
.createHash("md5")
.update(defaultValues.email || "example@example.com")
.digest("hex");
const formMethods = useForm<FormValues>({ const formMethods = useForm<FormValues>({
defaultValues, defaultValues,
resolver: zodResolver(profileFormSchema), resolver: zodResolver(profileFormSchema),
@@ -356,7 +350,7 @@ const ProfileForm = ({
name="avatar" name="avatar"
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<> <>
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" /> <Avatar alt="" imageSrc={value} gravatarFallbackMd5="fallback" size="lg" />
<div className="ltr:ml-4 rtl:mr-4"> <div className="ltr:ml-4 rtl:mr-4">
<ImageUploader <ImageUploader
target="avatar" target="avatar"
@@ -3,7 +3,7 @@ import { useState } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui"; import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText, Alert } from "@calcom/ui";
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal"; import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal"; import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
@@ -32,11 +32,14 @@ const TwoFactorAuthView = () => {
if (isLoading) return <SkeletonLoader />; if (isLoading) return <SkeletonLoader />;
const isCalProvider = user?.identityProvider === "CAL";
return ( return (
<> <>
<Meta title={t("2fa")} description={t("2fa_description")} /> <Meta title={t("2fa")} description={t("2fa_description")} />
{!isCalProvider && <Alert severity="neutral" message={t("2fa_disabled")} />}
<div className="mt-6 flex items-start space-x-4"> <div className="mt-6 flex items-start space-x-4">
<Switch <Switch
disabled={!isCalProvider}
checked={user?.twoFactorEnabled} checked={user?.twoFactorEnabled}
onCheckedChange={() => onCheckedChange={() =>
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true) user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
@@ -19,7 +19,7 @@ const OnboardTeamMembersPage = () => {
}; };
OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => ( OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => (
<WizardLayout currentStep={1} maxSteps={2}> <WizardLayout currentStep={2} maxSteps={2}>
{page} {page}
</WizardLayout> </WizardLayout>
); );
+9 -2
View File
@@ -3,7 +3,7 @@ import Head from "next/head";
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components"; import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getLayout } from "@components/layouts/WizardLayout"; import WizardLayout from "@components/layouts/WizardLayout";
const CreateNewTeamPage = () => { const CreateNewTeamPage = () => {
const { t } = useLocale(); const { t } = useLocale();
@@ -17,7 +17,14 @@ const CreateNewTeamPage = () => {
</> </>
); );
}; };
const LayoutWrapper = (page: React.ReactElement) => {
return (
<WizardLayout currentStep={1} maxSteps={2}>
{page}
</WizardLayout>
);
};
CreateNewTeamPage.getLayout = getLayout; CreateNewTeamPage.getLayout = LayoutWrapper;
export default CreateNewTeamPage; export default CreateNewTeamPage;
+10 -3
View File
@@ -10,7 +10,7 @@ import { CAL_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
import { md } from "@calcom/lib/markdownIt"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams"; import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
@@ -113,7 +113,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
<> <>
<div <div
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600" className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(team.bio || "") }} dangerouslySetInnerHTML={{ __html: team.safeBio }}
/> />
</> </>
)} )}
@@ -187,11 +187,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
...user, ...user,
avatar: CAL_URL + "/" + user.username + "/avatar.png", avatar: CAL_URL + "/" + user.username + "/avatar.png",
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(type.description),
})); }));
const safeBio = markdownToSafeHTML(team.bio) || "";
const members = team.members.map((member) => {
return { ...member, safeBio: markdownToSafeHTML(member.bio || "") };
});
return { return {
props: { props: {
team, team: { ...team, safeBio, members },
trpcState: ssr.dehydrate(), trpcState: ssr.dehydrate(),
}, },
} as const; } as const;
+3 -3
View File
@@ -2,9 +2,9 @@ import type { GetServerSidePropsContext } from "next";
import type { LocationObject } from "@calcom/core/location"; import type { LocationObject } from "@calcom/core/location";
import { privacyFilteredLocations } from "@calcom/core/location"; import { privacyFilteredLocations } from "@calcom/core/location";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import { getWorkingHours } from "@calcom/lib/availability"; import { getWorkingHours } from "@calcom/lib/availability";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -64,7 +64,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
select: { select: {
id: true, id: true,
name: true, name: true,
avatar: true,
username: true, username: true,
timeZone: true, timeZone: true,
hideBranding: true, hideBranding: true,
@@ -171,13 +170,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
hideBranding, hideBranding,
timeZone, timeZone,
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}); });
eventTypeObject.availability = []; eventTypeObject.availability = [];
let booking: GetBookingType | null = null; let booking: GetBookingType | null = null;
if (rescheduleUid) { if (rescheduleUid) {
booking = await getBooking(prisma, rescheduleUid, getBookingFieldsWithSystemFields(eventTypeObject)); booking = await getBooking(prisma, rescheduleUid);
} }
const weekStart = eventType.team?.members?.[0]?.user?.weekStart; const weekStart = eventType.team?.members?.[0]?.user?.weekStart;
+4 -1
View File
@@ -5,6 +5,7 @@ import type { LocationObject } from "@calcom/app-store/locations";
import { privacyFilteredLocations } from "@calcom/app-store/locations"; import { privacyFilteredLocations } from "@calcom/app-store/locations";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -121,13 +122,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
image: u.avatar, image: u.avatar,
slug: u.username, slug: u.username,
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}; };
})[0]; })[0];
let booking: GetBookingType | null = null; let booking: GetBookingType | null = null;
const { rescheduleUid, bookingUid } = querySchema.parse(context.query); const { rescheduleUid, bookingUid } = querySchema.parse(context.query);
if (rescheduleUid || bookingUid) { if (rescheduleUid || bookingUid) {
booking = await getBooking(prisma, rescheduleUid || bookingUid || "", eventTypeObject.bookingFields); booking = await getBooking(prisma, rescheduleUid || bookingUid || "");
} }
// Checking if number of recurring event ocurrances is valid against event type configuration // Checking if number of recurring event ocurrances is valid against event type configuration
@@ -152,6 +154,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
eventType: eventTypeObject, eventType: eventTypeObject,
recurringEventCount, recurringEventCount,
booking, booking,
currentSlotBooking: null,
isDynamicGroupBooking: false, isDynamicGroupBooking: false,
hasHashedBookingLink: false, hasHashedBookingLink: false,
hashedLink: null, hashedLink: null,
+1 -1
View File
@@ -205,7 +205,7 @@ test.describe("Booking with Seats", () => {
}, },
}); });
expect(oldBooking?.attendees).toBeFalsy(); expect(oldBooking?.status).toBe(BookingStatus.CANCELLED);
}); });
test("Should cancel with seats and have no attendees and cancelled", async ({ test("Should cancel with seats and have no attendees and cancelled", async ({
+1 -1
View File
@@ -119,7 +119,7 @@ test.describe("Event Types tests", () => {
await page.locator("#location-select").click(); await page.locator("#location-select").click();
await page.locator("text=In Person (Organizer Address)").click(); await page.locator("text=In Person (Organizer Address)").click();
// eslint-disable-next-line playwright/no-wait-for-timeout // eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(250); await page.waitForTimeout(1000);
await page.locator('input[name="locationAddress"]').fill(inputText); await page.locator('input[name="locationAddress"]').fill(inputText);
await page.locator("[data-testid=display-location]").check(); await page.locator("[data-testid=display-location]").check();
await page.locator("[data-testid=update-location]").click(); await page.locator("[data-testid=update-location]").click();
+1 -1
View File
@@ -45,7 +45,7 @@ export const createEmbedsFixture = (page: Page) => {
} }
api("on", { api("on", {
action: "*", action: "*",
callback: (e: any) => { callback: (e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work
+1 -1
View File
@@ -2,7 +2,7 @@ import type { Page } from "@playwright/test";
import { expect } from "@playwright/test"; import { expect } from "@playwright/test";
import type { IncomingMessage, ServerResponse } from "http"; import type { IncomingMessage, ServerResponse } from "http";
import { createServer } from "http"; import { createServer } from "http";
import noop from "lodash/noop"; import { noop } from "lodash";
import { test } from "./fixtures"; import { test } from "./fixtures";
+5 -1
View File
@@ -26,7 +26,11 @@ test.describe("Onboarding", () => {
await page.locator("input[name=username]").fill("new user onboarding"); await page.locator("input[name=username]").fill("new user onboarding");
await page.locator("input[name=name]").fill("new user 2"); await page.locator("input[name=name]").fill("new user 2");
await page.locator("input[role=combobox]").click(); await page.locator("input[role=combobox]").click();
await page.locator("text=Europe/London GMT +0:00").click(); await page
.locator("*")
.filter({ hasText: /^Europe\/London/ })
.first()
.click();
await page.locator("button[type=submit]").click(); await page.locator("button[type=submit]").click();
+11 -7
View File
@@ -97,12 +97,19 @@ test.describe("Reschedule Tests", async () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible(); await expect(page.locator("[data-testid=success-page]")).toBeVisible();
// NOTE: remove if old booking should not be deleted const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: booking.uid } });
expect(await (await booking.self()).status).toBe(BookingStatus.CANCELLED); const rescheduledBooking = await prisma.booking.findFirstOrThrow({ where: { uid: booking.uid } });
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking.uid } });
expect(newBooking).not.toBeNull(); expect(newBooking).not.toBeNull();
await prisma.booking.delete({ where: { id: newBooking?.id } }); expect(rescheduledBooking.status).toBe(BookingStatus.CANCELLED);
await prisma.booking.deleteMany({
where: {
id: {
in: [newBooking.id, rescheduledBooking.id],
},
},
});
}); });
test("Unpaid rescheduling should go to payment page", async ({ page, users, bookings, payments }) => { test("Unpaid rescheduling should go to payment page", async ({ page, users, bookings, payments }) => {
@@ -148,7 +155,6 @@ test.describe("Reschedule Tests", async () => {
}); });
await expect(page).toHaveURL(/.*payment/); await expect(page).toHaveURL(/.*payment/);
await payment.delete();
}); });
test("Paid rescheduling should go to success page", async ({ page, users, bookings, payments }) => { test("Paid rescheduling should go to success page", async ({ page, users, bookings, payments }) => {
@@ -172,8 +178,6 @@ test.describe("Reschedule Tests", async () => {
await page.locator('[data-testid="confirm-reschedule-button"]').click(); await page.locator('[data-testid="confirm-reschedule-button"]').click();
await expect(page).toHaveURL(/.*booking/); await expect(page).toHaveURL(/.*booking/);
await payment.delete();
}); });
test("Opt in event should be PENDING when rescheduled by USER", async ({ page, users, bookings }) => { test("Opt in event should be PENDING when rescheduled by USER", async ({ page, users, bookings }) => {
+2
View File
@@ -5,3 +5,5 @@ Disallow: /static/locales
# Specifically allow access to OG Image api, otherwise eg Twitter won't render these images. # Specifically allow access to OG Image api, otherwise eg Twitter won't render these images.
Allow: /api/social/og/image Allow: /api/social/og/image
# Specifically allow access to api/app-store to allow search engines to fetch assets
Allow: /api/app-store/
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

+37 -3
View File
@@ -404,6 +404,7 @@
"add_time_availability": "Add new time slot", "add_time_availability": "Add new time slot",
"add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.", "add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.",
"2fa": "Two-Factor Authentication", "2fa": "Two-Factor Authentication",
"2fa_disabled":"Two-Factor authentication can only be enabled for email and password authentication",
"enable_2fa": "Enable two-factor authentication", "enable_2fa": "Enable two-factor authentication",
"disable_2fa": "Disable two-factor authentication", "disable_2fa": "Disable two-factor authentication",
"disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.", "disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.",
@@ -1218,6 +1219,7 @@
"impersonation": "Impersonation", "impersonation": "Impersonation",
"impersonation_description": "Settings to manage user impersonation", "impersonation_description": "Settings to manage user impersonation",
"users": "Users", "users": "Users",
"user": "User",
"profile_description": "Manage settings for your {{appName}} profile", "profile_description": "Manage settings for your {{appName}} profile",
"users_description": "Here you can find a list of all users", "users_description": "Here you can find a list of all users",
"users_listing": "User listing", "users_listing": "User listing",
@@ -1499,7 +1501,7 @@
"date_overrides_update_btn": "Update Override", "date_overrides_update_btn": "Update Override",
"event_type_duplicate_copy_text": "{{slug}}-copy", "event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "Set as default", "set_as_default": "Set as default",
"hide_eventtype_details": "Hide EventType Details", "hide_eventtype_details": "Hide event type details",
"show_navigation": "Show navigation", "show_navigation": "Show navigation",
"hide_navigation": "Hide navigation", "hide_navigation": "Hide navigation",
"verification_code_sent": "Verification code sent", "verification_code_sent": "Verification code sent",
@@ -1513,7 +1515,7 @@
"create_your_first_team_webhook_description": "Create your first webhook for this team event type", "create_your_first_team_webhook_description": "Create your first webhook for this team event type",
"create_webhook_team_event_type": "Create a webhook for this team event type", "create_webhook_team_event_type": "Create a webhook for this team event type",
"disable_success_page": "Disable Success Page (only works if you have a redirect URL)", "disable_success_page": "Disable Success Page (only works if you have a redirect URL)",
"invalid_admin_password": "You are set as an admin but you do not have a password length of at least 15 characters", "invalid_admin_password": "You are admin but you do not have a password length of at least 15 characters or no 2FA yet",
"change_password_admin": "Change Password to gain admin access", "change_password_admin": "Change Password to gain admin access",
"username_already_taken": "Username is already taken", "username_already_taken": "Username is already taken",
"assignment": "Assignment", "assignment": "Assignment",
@@ -1608,6 +1610,7 @@
"default_app_link_title": "Set a default app link", "default_app_link_title": "Set a default app link",
"default_app_link_description": "Setting a default app link allows all newly created event types to use the app link you set.", "default_app_link_description": "Setting a default app link allows all newly created event types to use the app link you set.",
"change_default_conferencing_app": "Set as default", "change_default_conferencing_app": "Set as default",
"organizer_default_conferencing_app": "Organizer's default app",
"under_maintenance": "Down for maintenance", "under_maintenance": "Down for maintenance",
"under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.", "under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.",
"event_type_seats": "{{numberOfSeats}} seats", "event_type_seats": "{{numberOfSeats}} seats",
@@ -1647,6 +1650,14 @@
"add_1_option_per_line": "Add 1 option per line", "add_1_option_per_line": "Add 1 option per line",
"select_a_router": "Select a router", "select_a_router": "Select a router",
"add_a_new_route": "Add a new Route", "add_a_new_route": "Add a new Route",
"make_informed_decisions": "Make informed decisions with Insights",
"make_informed_decisions_description": "Our Insights dashboard surfaces all activity across your team and shows you trends that enable better team scheduling and decision making.",
"view_bookings_across": "View bookings across all members",
"view_bookings_across_description": "See who’s receiving the most bookings and ensure the best distribution across your team",
"identify_booking_trends": "Identify booking trends",
"identify_booking_trends_description": "See what times of the week and what times during the day are popular for your bookers",
"spot_popular_event_types": "Spot popular event types",
"spot_popular_event_types_description": "See which of your event types are receiving the most clicks and bookings",
"no_responses_yet": "No responses yet", "no_responses_yet": "No responses yet",
"this_will_be_the_placeholder": "This will be the placeholder", "this_will_be_the_placeholder": "This will be the placeholder",
"this_meeting_has_not_started_yet": "This meeting has not started yet", "this_meeting_has_not_started_yet": "This meeting has not started yet",
@@ -1660,5 +1671,28 @@
"invalid_event_name_variables": "There is an invalid variable in your event name", "invalid_event_name_variables": "There is an invalid variable in your event name",
"select_all": "Select All", "select_all": "Select All",
"default_conferencing_bulk_title": "Bulk update existing event types", "default_conferencing_bulk_title": "Bulk update existing event types",
"default_conferencing_bulk_description": "Update the locations for the selected event types" "requires_at_least_one_schedule": "You are required to have at least one schedule",
"default_conferencing_bulk_description": "Update the locations for the selected event types",
"looking_for_more_analytics": "Looking for more analytics?",
"add_filter": "Add filter",
"select_user": "Select User",
"select_event_type": "Select Event Type",
"select_date_range": "Select Date Range",
"popular_events": "Popular Events",
"no_event_types_found": "No event types found",
"average_event_duration": "Average Event Duration",
"most_booked_members": "Most Booked Members",
"least_booked_members": "Least Booked Members",
"events_created": "Events Created",
"events_completed": "Events Completed",
"events_cancelled": "Events Cancelled",
"events_rescheduled": "Events Rescheduled",
"from_last_period": "from last period",
"from_to_date_period": "From: {{startDate}} To: {{endDate}}",
"analytics_for_organisation": "Insights for {{organisationName}}",
"subtitle_analytics": "Learn more about your team's activity",
"event_trends": "Event Trends",
"clear_filters": "Clear Filters",
"insights": "Insights",
"email_invite_team": "{{email}} has been invited"
} }
@@ -1213,6 +1213,7 @@
"impersonation": "Suplantación", "impersonation": "Suplantación",
"impersonation_description": "Configuración para administrar la suplantación del usuario", "impersonation_description": "Configuración para administrar la suplantación del usuario",
"users": "Usuarios", "users": "Usuarios",
"user": "Usuario",
"profile_description": "Administra los ajustes para tu perfil de {{appName}}", "profile_description": "Administra los ajustes para tu perfil de {{appName}}",
"users_description": "Aquí puede encontrar una lista de todos los usuarios", "users_description": "Aquí puede encontrar una lista de todos los usuarios",
"users_listing": "Lista de usuarios", "users_listing": "Lista de usuarios",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

-1
View File
@@ -29,7 +29,6 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
ctx: { ctx: {
prisma, prisma,
session: null, session: null,
user: null,
locale, locale,
i18n: _i18n, i18n: _i18n,
}, },
+2
View File
@@ -497,3 +497,5 @@ hr {
::-webkit-search-cancel-button { ::-webkit-search-cancel-button {
-webkit-appearance: none; -webkit-appearance: none;
} }
+1 -1
View File
@@ -2,5 +2,5 @@ const base = require("@calcom/config/tailwind-preset");
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
...base, ...base,
content: [...base.content], content: [...base.content, "../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}"],
}; };
+3 -1
View File
@@ -70,6 +70,7 @@
"test-playwright": "yarn playwright test --config=playwright.config.ts", "test-playwright": "yarn playwright test --config=playwright.config.ts",
"test": "jest", "test": "jest",
"type-check": "turbo run type-check", "type-check": "turbo run type-check",
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
"web": "yarn workspace @calcom/web" "web": "yarn workspace @calcom/web"
}, },
"devDependencies": { "devDependencies": {
@@ -86,8 +87,9 @@
"jest-watch-select-projects": "^2.0.0", "jest-watch-select-projects": "^2.0.0",
"jest-watch-typeahead": "^2.0.0", "jest-watch-typeahead": "^2.0.0",
"lint-staged": "^12.5.0", "lint-staged": "^12.5.0",
"prettier": "^2.8.4", "prettier": "^2.8.6",
"ts-jest": "^28.0.8", "ts-jest": "^28.0.8",
"tsc-absolute": "^1.0.0",
"typescript": "^4.9.4" "typescript": "^4.9.4"
}, },
"dependencies": { "dependencies": {
+3 -1
View File
@@ -1,7 +1,7 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json"; import type seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json";
import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants"; import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants";
import execSync from "./utils/execSync"; import execSync from "./utils/execSync";
@@ -127,6 +127,8 @@ export const BaseAppFork = {
.replace(/_DESCRIPTION_/g, description) .replace(/_DESCRIPTION_/g, description)
.replace(/_APP_DIR_/g, slug) .replace(/_APP_DIR_/g, slug)
); );
// New monorepo package has been added, so we need to run yarn again
await execSync("yarn");
}, },
delete: async function ({ slug, isTemplate }: { slug: string; isTemplate: boolean }) { delete: async function ({ slug, isTemplate }: { slug: string; isTemplate: boolean }) {
+1
View File
@@ -10,6 +10,7 @@ items:
{DESCRIPTION} {DESCRIPTION}
### Cron's most valuable features ### Cron's most valuable features
<!-- From "FAQs: What are Cron’s most valuable features?" <!-- From "FAQs: What are Cron’s most valuable features?"
https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 --> https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 -->
@@ -133,6 +133,7 @@ export default class GoogleCalendarService implements Calendar {
calendarId: selectedCalendar, calendarId: selectedCalendar,
requestBody: payload, requestBody: payload,
conferenceDataVersion: 1, conferenceDataVersion: 1,
sendUpdates: "none",
}, },
function (error, event) { function (error, event) {
if (error || !event?.data) { if (error || !event?.data) {
@@ -164,6 +165,7 @@ export default class GoogleCalendarService implements Calendar {
type: "google_calendar", type: "google_calendar",
password: "", password: "",
url: "", url: "",
iCalUID: event.data.iCalUID,
}); });
} }
); );
@@ -200,6 +202,9 @@ export default class GoogleCalendarService implements Calendar {
id: String(event.organizer.id), id: String(event.organizer.id),
organizer: true, organizer: true,
responseStatus: "accepted", responseStatus: "accepted",
email: event.destinationCalendar?.externalId
? event.destinationCalendar.externalId
: event.organizer.email,
}, },
// eslint-disable-next-line // eslint-disable-next-line
...eventAttendees, ...eventAttendees,
@@ -234,14 +239,13 @@ export default class GoogleCalendarService implements Calendar {
calendarId: selectedCalendar, calendarId: selectedCalendar,
eventId: uid, eventId: uid,
sendNotifications: true, sendNotifications: true,
sendUpdates: "all", sendUpdates: "none",
requestBody: payload, requestBody: payload,
conferenceDataVersion: 1, conferenceDataVersion: 1,
}, },
function (err, evt) { function (err, evt) {
if (err) { if (err) {
console.error("There was an error contacting google calendar service: ", err); console.error("There was an error contacting google calendar service: ", err);
return reject(err); return reject(err);
} }
@@ -268,6 +272,7 @@ export default class GoogleCalendarService implements Calendar {
type: "google_calendar", type: "google_calendar",
password: "", password: "",
url: "", url: "",
iCalUID: evt.data.iCalUID,
}); });
} }
return resolve(evt?.data); return resolve(evt?.data);
@@ -293,7 +298,7 @@ export default class GoogleCalendarService implements Calendar {
calendarId: calendarId ? calendarId : defaultCalendarId, calendarId: calendarId ? calendarId : defaultCalendarId,
eventId: uid, eventId: uid,
sendNotifications: false, sendNotifications: false,
sendUpdates: "all", sendUpdates: "none",
}, },
function (err: GoogleCalError | null, event) { function (err: GoogleCalError | null, event) {
if (err) { if (err) {
+25 -4
View File
@@ -12,13 +12,20 @@ export type DefaultEventLocationType = {
type: DefaultEventLocationTypeEnum; type: DefaultEventLocationTypeEnum;
label: string; label: string;
messageForOrganizer: string; messageForOrganizer: string;
category: "in person" | "other" | "phone"; category: "in person" | "conferencing" | "other" | "phone";
iconUrl: string; iconUrl: string;
urlRegExp?: string; urlRegExp?: string;
// HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places. // HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places.
variable: "locationType" | "locationAddress" | "address" | "locationLink" | "locationPhoneNumber" | "phone"; variable:
defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "phone"; | "locationType"
| "locationAddress"
| "address"
| "locationLink"
| "locationPhoneNumber"
| "phone"
| "hostDefault";
defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone";
} & ( } & (
| { | {
organizerInputType: "phone" | "text" | null; organizerInputType: "phone" | "text" | null;
@@ -60,6 +67,7 @@ export enum DefaultEventLocationTypeEnum {
*/ */
UserPhone = "userPhone", UserPhone = "userPhone",
Link = "link", Link = "link",
Conferencing = "conferencing",
} }
export const defaultLocations: DefaultEventLocationType[] = [ export const defaultLocations: DefaultEventLocationType[] = [
@@ -88,6 +96,17 @@ export const defaultLocations: DefaultEventLocationType[] = [
iconUrl: "/map-pin.svg", iconUrl: "/map-pin.svg",
category: "in person", category: "in person",
}, },
{
default: true,
type: DefaultEventLocationTypeEnum.Conferencing,
iconUrl: "/link.svg",
organizerInputType: null,
label: "organizer_default_conferencing_app",
variable: "hostDefault",
defaultValueVariable: "hostDefault",
category: "conferencing",
messageForOrganizer: "",
},
{ {
default: true, default: true,
type: DefaultEventLocationTypeEnum.Link, type: DefaultEventLocationTypeEnum.Link,
@@ -130,7 +149,9 @@ export const defaultLocations: DefaultEventLocationType[] = [
export type LocationObject = { export type LocationObject = {
type: string; type: string;
displayLocationPublicly?: boolean; displayLocationPublicly?: boolean;
} & Partial<Record<"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "phone", string>>; } & Partial<
Record<"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone", string>
>;
// integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video // integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video
export type BookingLocationValue = string; export type BookingLocationValue = string;
@@ -73,7 +73,9 @@ export default class Office365CalendarService implements Calendar {
body: JSON.stringify(this.translateEvent(event)), body: JSON.stringify(this.translateEvent(event)),
}); });
return handleErrorsJson(response); const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
return { ...responseJson, iCalUID: responseJson.iCalUId };
} catch (error) { } catch (error) {
this.log.error(error); this.log.error(error);
@@ -88,7 +90,9 @@ export default class Office365CalendarService implements Calendar {
body: JSON.stringify(this.translateEvent(event)), body: JSON.stringify(this.translateEvent(event)),
}); });
return handleErrorsRaw(response); const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
return { ...responseJson, iCalUID: responseJson.iCalUId };
} catch (error) { } catch (error) {
this.log.error(error); this.log.error(error);
@@ -219,7 +223,7 @@ export default class Office365CalendarService implements Calendar {
}; };
const o365AuthCredentials = credential.key as O365AuthCredentials; const o365AuthCredentials = credential.key as O365AuthCredentials;
const refreshAccessToken = async (refreshToken: string) => { const refreshAccessToken = async (o365AuthCredentials: O365AuthCredentials) => {
const { client_id, client_secret } = await getOfficeAppKeys(); const { client_id, client_secret } = await getOfficeAppKeys();
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", { const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
method: "POST", method: "POST",
@@ -227,12 +231,22 @@ export default class Office365CalendarService implements Calendar {
body: new URLSearchParams({ body: new URLSearchParams({
scope: "User.Read Calendars.Read Calendars.ReadWrite", scope: "User.Read Calendars.Read Calendars.ReadWrite",
client_id, client_id,
refresh_token: refreshToken, refresh_token: o365AuthCredentials.refresh_token,
grant_type: "refresh_token", grant_type: "refresh_token",
client_secret, client_secret,
}), }),
}); });
const o365AuthCredentials = refreshTokenResponseSchema.parse(await handleErrorsJson(response)); const responseJson = await handleErrorsJson(response);
const tokenResponse = refreshTokenResponseSchema.safeParse(responseJson);
o365AuthCredentials = { ...o365AuthCredentials, ...(tokenResponse.success && tokenResponse.data) };
if (!tokenResponse.success) {
console.error(
"Outlook error grabbing new tokens ~ zodError:",
tokenResponse.error,
"MS response:",
responseJson
);
}
await prisma.credential.update({ await prisma.credential.update({
where: { where: {
id: credential.id, id: credential.id,
@@ -246,9 +260,10 @@ export default class Office365CalendarService implements Calendar {
return { return {
getToken: () => getToken: () =>
refreshTokenResponseSchema.safeParse(o365AuthCredentials).success &&
!isExpired(o365AuthCredentials.expires_in) !isExpired(o365AuthCredentials.expires_in)
? Promise.resolve(o365AuthCredentials.access_token) ? Promise.resolve(o365AuthCredentials.access_token)
: refreshAccessToken(o365AuthCredentials.refresh_token), : refreshAccessToken(o365AuthCredentials),
}; };
}; };
+1
View File
@@ -8,6 +8,7 @@
], ],
"scripts": { "scripts": {
"type-check": "tsc --pretty --noEmit", "type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit",
"lint": "eslint . --ext .ts,.js,.tsx,.jsx", "lint": "eslint . --ext .ts,.js,.tsx,.jsx",
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix", "lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json" "lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json"
@@ -4,6 +4,8 @@ import type { Settings, Widgets, SelectWidgetProps } from "react-awesome-query-b
//@ts-ignore //@ts-ignore
import BasicConfig from "react-awesome-query-builder/lib/config/basic"; import BasicConfig from "react-awesome-query-builder/lib/config/basic";
import { EmailField } from "@calcom/ui";
import widgetsComponents from "../widgets"; import widgetsComponents from "../widgets";
const { const {
@@ -92,8 +94,7 @@ const widgets: Widgets & { [key in keyof Widgets]: Widgets[key] & { type: string
if (!props) { if (!props) {
return <div />; return <div />;
} }
// TODO: Use EmailField component for Routing Form Email field return <EmailField type="email" {...props} />;
return <TextWidget type="email" {...props} />;
}, },
}, },
}; };
@@ -99,23 +99,18 @@ export default function RoutingForms({
CTA={hasPaidPlan && <NewFormButton />} CTA={hasPaidPlan && <NewFormButton />}
subtitle={t("routing_forms_description")}> subtitle={t("routing_forms_description")}>
<UpgradeTip <UpgradeTip
dark
title={t("teams_plan_required")} title={t("teams_plan_required")}
description={t("routing_forms_are_a_great_way")} description={t("routing_forms_are_a_great_way")}
features={features} features={features}
background="/routing-form-banner-background.jpg" background="/tips/routing-forms.jpg"
isParentLoading={isLoading && <SkeletonLoaderTeamList />} isParentLoading={isLoading && <SkeletonLoaderTeamList />}
buttons={ buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2"> <div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup> <ButtonGroup>
<Button color="secondary" href={`${WEBAPP_URL}/settings/teams/new`}> <Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("upgrade")} {t("upgrade")}
</Button> </Button>
<Button <Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
color="minimal"
className="!bg-transparent text-white opacity-50 hover:opacity-100"
href="https://go.cal.com/teams-video"
target="_blank">
{t("learn_more")} {t("learn_more")}
</Button> </Button>
</ButtonGroup> </ButtonGroup>
Whitespace-only changes.
+1 -1
View File
@@ -19,7 +19,7 @@
"type": "integrations:whereby_video", "type": "integrations:whereby_video",
"label": "Whereby Video", "label": "Whereby Video",
"organizerInputPlaceholder": "https://www.whereby.com/cal", "organizerInputPlaceholder": "https://www.whereby.com/cal",
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?(team.)?whereby.com\\/[a-zA-Z0-9]*" "urlRegExp": "^(?:https?://)?(?:(?!.*-\\.)(?:\\w+(-\\w+)*\\.))*whereby\\.com(/[\\w\\-._~:?#\\[\\]@!$&'()*+,;%=]+)*$"
} }
} }
} }
-14
View File
@@ -1,14 +0,0 @@
/* @see https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/31#issuecomment-1024722576 */
const tailwind = require("prettier-plugin-tailwindcss");
const sortImports = require("@trivago/prettier-plugin-sort-imports");
const combinedFormatter = {
parsers: {
typescript: {
...tailwind.parsers.typescript,
preprocess: sortImports.parsers.typescript.preprocess,
},
},
};
module.exports = combinedFormatter;
+3 -3
View File
@@ -20,13 +20,13 @@
"@tailwindcss/forms": "^0.5.2", "@tailwindcss/forms": "^0.5.2",
"@tailwindcss/line-clamp": "^0.4.0", "@tailwindcss/line-clamp": "^0.4.0",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.4",
"@trivago/prettier-plugin-sort-imports": "3.2.0", "@trivago/prettier-plugin-sort-imports": "4.1.1",
"@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0", "@typescript-eslint/parser": "^5.52.0",
"eslint": "^8.34.0", "eslint": "^8.34.0",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"prettier": "^2.8.4", "prettier": "^2.8.6",
"prettier-plugin-tailwindcss": "^0.1.13", "prettier-plugin-tailwindcss": "^0.2.5",
"tailwind-scrollbar": "^2.0.1", "tailwind-scrollbar": "^2.0.1",
"tailwindcss": "^3.2.1", "tailwindcss": "^3.2.1",
"typescript": "^4.9.4" "typescript": "^4.9.4"
+8 -1
View File
@@ -16,7 +16,14 @@ module.exports = {
"^[./]", "^[./]",
], ],
importOrderSeparation: true, importOrderSeparation: true,
plugins: [require("./merged-prettier-plugin")], plugins: [
"@trivago/prettier-plugin-sort-imports",
/**
* **NOTE** tailwind plugin must come last!
* @see https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
*/
"prettier-plugin-tailwindcss",
],
overrides: [ overrides: [
{ {
files: ["apps/website/lib/utils/wordlist/wordlist.ts"], files: ["apps/website/lib/utils/wordlist/wordlist.ts"],
+10 -7
View File
@@ -1,5 +1,5 @@
import type { SelectedCalendar } from "@prisma/client"; import type { SelectedCalendar } from "@prisma/client";
import _ from "lodash"; import { sortBy } from "lodash";
import * as process from "process"; import * as process from "process";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
@@ -54,8 +54,8 @@ export const getConnectedCalendars = async (
}; };
} }
const cals = await calendar.listCalendars(); const cals = await calendar.listCalendars();
const calendars = _(cals) const calendars = sortBy(
.map((cal) => { cals.map((cal) => {
if (cal.externalId === destinationCalendarExternalId) destinationCalendar = cal; if (cal.externalId === destinationCalendarExternalId) destinationCalendar = cal;
return { return {
...cal, ...cal,
@@ -64,9 +64,9 @@ export const getConnectedCalendars = async (
isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId), isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId),
credentialId, credentialId,
}; };
}) }),
.sortBy(["primary"]) ["primary"]
.value(); );
const primary = calendars.find((item) => item.primary) ?? calendars.find((cal) => cal !== undefined); const primary = calendars.find((item) => item.primary) ?? calendars.find((cal) => cal !== undefined);
if (!primary) { if (!primary) {
return { return {
@@ -77,9 +77,11 @@ export const getConnectedCalendars = async (
}, },
}; };
} }
if (destinationCalendar) { // HACK https://github.com/calcom/cal.com/pull/7644/files#r1131508414
if (destinationCalendar && !Object.isFrozen(destinationCalendar)) {
destinationCalendar.primaryEmail = primary.email; destinationCalendar.primaryEmail = primary.email;
destinationCalendar.integrationTitle = integration.title; destinationCalendar.integrationTitle = integration.title;
destinationCalendar = Object.freeze(destinationCalendar);
} }
return { return {
@@ -263,6 +265,7 @@ export const createEvent = async (
type: credential.type, type: credential.type,
success, success,
uid, uid,
iCalUID: creationResult?.iCalUID || undefined,
createdEvent: creationResult, createdEvent: creationResult,
originalEvent: calEvent, originalEvent: calEvent,
calError, calError,
+17 -9
View File
@@ -1,6 +1,5 @@
import type { DestinationCalendar, Booking } from "@prisma/client"; import type { DestinationCalendar, Booking } from "@prisma/client";
import { cloneDeep } from "lodash"; import { cloneDeep, merge } from "lodash";
import merge from "lodash/merge";
import { v5 as uuidv5 } from "uuid"; import { v5 as uuidv5 } from "uuid";
import type { z } from "zod"; import type { z } from "zod";
@@ -11,15 +10,12 @@ import { MeetLocationType } from "@calcom/app-store/locations";
import getApps from "@calcom/app-store/utils"; import getApps from "@calcom/app-store/utils";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { createdEventSchema } from "@calcom/prisma/zod-utils"; import { createdEventSchema } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar"; import type { NewCalendarEventType } from "@calcom/types/Calendar";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential"; import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
import type { Event } from "@calcom/types/Event"; import type { Event } from "@calcom/types/Event";
import type { import type { EventResult } from "@calcom/types/EventManager";
CreateUpdateResult, import type { CreateUpdateResult, PartialBooking, PartialReference } from "@calcom/types/EventManager";
EventResult,
PartialBooking,
PartialReference,
} from "@calcom/types/EventManager";
import { createEvent, updateEvent } from "./CalendarManager"; import { createEvent, updateEvent } from "./CalendarManager";
import { createMeeting, updateMeeting } from "./videoClient"; import { createMeeting, updateMeeting } from "./videoClient";
@@ -126,12 +122,24 @@ export default class EventManager {
// Create the calendar event with the proper video call data // Create the calendar event with the proper video call data
results.push(...(await this.createAllCalendarEvents(clonedCalEvent))); results.push(...(await this.createAllCalendarEvents(clonedCalEvent)));
// Since the result can be a new calendar event or video event, we have to create a type guard
// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
const isCalendarResult = (
result: (typeof results)[number]
): result is EventResult<NewCalendarEventType> => {
return result.type.includes("_calendar");
};
const referencesToCreate = results.map((result) => { const referencesToCreate = results.map((result) => {
let createdEventObj: createdEventSchema | null = null; let createdEventObj: createdEventSchema | null = null;
if (typeof result?.createdEvent === "string") { if (typeof result?.createdEvent === "string") {
createdEventObj = createdEventSchema.parse(JSON.parse(result.createdEvent)); createdEventObj = createdEventSchema.parse(JSON.parse(result.createdEvent));
} }
if (isCalendarResult(result)) {
evt.iCalUID = result.iCalUID || undefined;
}
return { return {
type: result.type, type: result.type,
uid: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString() ?? "", uid: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString() ?? "",
-25
View File
@@ -1,31 +1,6 @@
/* eslint-disable @calcom/eslint/deprecated-imports */ /* eslint-disable @calcom/eslint/deprecated-imports */
import dayjs from "dayjs"; import dayjs from "dayjs";
import dayjsBusinessTime from "dayjs-business-days2"; import dayjsBusinessTime from "dayjs-business-days2";
import "dayjs/locale/ar";
import "dayjs/locale/bg";
import "dayjs/locale/cs";
import "dayjs/locale/de";
import "dayjs/locale/es";
import "dayjs/locale/es-mx";
import "dayjs/locale/fr";
import "dayjs/locale/he";
import "dayjs/locale/hu";
import "dayjs/locale/it";
import "dayjs/locale/ja";
import "dayjs/locale/ko";
import "dayjs/locale/nl";
import "dayjs/locale/pl";
import "dayjs/locale/pt";
import "dayjs/locale/pt-br";
import "dayjs/locale/ro";
import "dayjs/locale/ru";
import "dayjs/locale/sr";
import "dayjs/locale/sv";
import "dayjs/locale/tr";
import "dayjs/locale/uk";
import "dayjs/locale/vi";
import "dayjs/locale/zh-cn";
import "dayjs/locale/zh-tw";
import customParseFormat from "dayjs/plugin/customParseFormat"; import customParseFormat from "dayjs/plugin/customParseFormat";
import isBetween from "dayjs/plugin/isBetween"; import isBetween from "dayjs/plugin/isBetween";
import isToday from "dayjs/plugin/isToday"; import isToday from "dayjs/plugin/isToday";
+25
View File
@@ -0,0 +1,25 @@
import "dayjs/locale/ar";
import "dayjs/locale/bg";
import "dayjs/locale/cs";
import "dayjs/locale/de";
import "dayjs/locale/es";
import "dayjs/locale/es-mx";
import "dayjs/locale/fr";
import "dayjs/locale/he";
import "dayjs/locale/hu";
import "dayjs/locale/it";
import "dayjs/locale/ja";
import "dayjs/locale/ko";
import "dayjs/locale/nl";
import "dayjs/locale/pl";
import "dayjs/locale/pt";
import "dayjs/locale/pt-br";
import "dayjs/locale/ro";
import "dayjs/locale/ru";
import "dayjs/locale/sr";
import "dayjs/locale/sv";
import "dayjs/locale/tr";
import "dayjs/locale/uk";
import "dayjs/locale/vi";
import "dayjs/locale/zh-cn";
import "dayjs/locale/zh-tw";
+20 -1
View File
@@ -1,3 +1,5 @@
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
const Spacer = () => <p style={{ height: 6 }} />; const Spacer = () => <p style={{ height: 6 }} />;
export const Info = (props: { export const Info = (props: {
@@ -6,8 +8,14 @@ export const Info = (props: {
extraInfo?: React.ReactNode; extraInfo?: React.ReactNode;
withSpacer?: boolean; withSpacer?: boolean;
lineThrough?: boolean; lineThrough?: boolean;
formatted?: boolean;
}) => { }) => {
if (!props.description || props.description === "") return null; if (!props.description || props.description === "") return null;
const descriptionCSS = "color: '#101010'; font-weight: 400; line-height: 24px; margin: 0;";
const safeDescription = markdownToSafeHTML(props.description.toString()) || "";
return ( return (
<> <>
{props.withSpacer && <Spacer />} {props.withSpacer && <Spacer />}
@@ -21,7 +29,18 @@ export const Info = (props: {
whiteSpace: "pre-wrap", whiteSpace: "pre-wrap",
textDecoration: props.lineThrough ? "line-through" : undefined, textDecoration: props.lineThrough ? "line-through" : undefined,
}}> }}>
{props.description} {props.formatted ? (
<p
className="dark:text-darkgray-600 mt-2 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{
__html: safeDescription
.replaceAll("<p>", `<p style="${descriptionCSS}">`)
.replaceAll("<li>", `<li style="${descriptionCSS}">`),
}}
/>
) : (
props.description
)}
</p> </p>
{props.extraInfo} {props.extraInfo}
</div> </div>
@@ -2,6 +2,8 @@ import type { TFunction } from "next-i18next";
import { RRule } from "rrule"; import { RRule } from "rrule";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
// TODO: Use browser locale, implement Intl in Dayjs maybe?
import "@calcom/dayjs/locales";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { TimeFormat } from "@calcom/lib/timeFormat"; import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import type { CalendarEvent, Person } from "@calcom/types/Calendar";
@@ -76,7 +76,7 @@ export const BaseScheduledEmail = (
<WhenInfo calEvent={props.calEvent} t={t} timeZone={timeZone} /> <WhenInfo calEvent={props.calEvent} t={t} timeZone={timeZone} />
<WhoInfo calEvent={props.calEvent} t={t} /> <WhoInfo calEvent={props.calEvent} t={t} />
<LocationInfo calEvent={props.calEvent} t={t} /> <LocationInfo calEvent={props.calEvent} t={t} />
<Info label={t("description")} description={props.calEvent.description} withSpacer /> <Info label={t("description")} description={props.calEvent.description} withSpacer formatted />
<Info label={t("additional_notes")} description={props.calEvent.additionalNotes} withSpacer /> <Info label={t("additional_notes")} description={props.calEvent.additionalNotes} withSpacer />
{props.includeAppsStatus && <AppsStatus calEvent={props.calEvent} t={t} />} {props.includeAppsStatus && <AppsStatus calEvent={props.calEvent} t={t} />}
<UserFieldsResponses calEvent={props.calEvent} /> <UserFieldsResponses calEvent={props.calEvent} />
+9 -1
View File
@@ -4,8 +4,10 @@ import { z } from "zod";
import type { Dayjs } from "@calcom/dayjs"; import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getErrorFromUnknown } from "@calcom/lib/errors"; import { getErrorFromUnknown } from "@calcom/lib/errors";
import { serverConfig } from "@calcom/lib/serverConfig"; import { serverConfig } from "@calcom/lib/serverConfig";
import prisma from "@calcom/prisma";
declare let global: { declare let global: {
E2E_EMAILS?: Record<string, unknown>[]; E2E_EMAILS?: Record<string, unknown>[];
@@ -29,7 +31,13 @@ export default class BaseEmail {
protected getNodeMailerPayload(): Record<string, unknown> { protected getNodeMailerPayload(): Record<string, unknown> {
return {}; return {};
} }
public sendEmail() { public async sendEmail() {
const featureFlags = await getFeatureFlagMap(prisma);
/** If email kill switch exists and is active, we prevent emails being sent. */
if (featureFlags.emails) {
console.warn("Skipped Sending Email due to active Kill Switch");
return new Promise((r) => r("Skipped Sending Email due to active Kill Switch"));
}
if (process.env.NEXT_PUBLIC_IS_E2E) { if (process.env.NEXT_PUBLIC_IS_E2E) {
global.E2E_EMAILS = global.E2E_EMAILS || []; global.E2E_EMAILS = global.E2E_EMAILS || [];
global.E2E_EMAILS.push(this.getNodeMailerPayload()); global.E2E_EMAILS.push(this.getNodeMailerPayload());
@@ -16,20 +16,18 @@ export default class AttendeeScheduledEmail extends BaseEmail {
attendee: Person; attendee: Person;
showAttendees: boolean | undefined; showAttendees: boolean | undefined;
t: TFunction; t: TFunction;
attendees: Person[];
constructor(calEvent: CalendarEvent, attendee: Person, showAttendees?: boolean | undefined) { constructor(calEvent: CalendarEvent, attendee: Person, showAttendees?: boolean | undefined) {
super(); super();
this.name = "SEND_BOOKING_CONFIRMATION"; if (!showAttendees && calEvent.seatsPerTimeSlot) {
this.calEvent = cloneDeep(calEvent);
this.calEvent.attendees = [attendee];
} else {
this.calEvent = calEvent; this.calEvent = calEvent;
this.attendee = attendee;
this.showAttendees = showAttendees;
this.t = attendee.language.translate;
this.attendees = [...this.calEvent.attendees];
if (!this.showAttendees && this.calEvent.seatsPerTimeSlot) {
this.attendees = [this.attendee];
this.calEvent.attendees = [this.attendee];
} }
this.name = "SEND_BOOKING_CONFIRMATION";
this.attendee = attendee;
this.t = attendee.language.translate;
} }
protected getiCalEventAsString(): string | undefined { protected getiCalEventAsString(): string | undefined {
@@ -39,21 +37,21 @@ export default class AttendeeScheduledEmail extends BaseEmail {
// ics appends "RRULE:" already, so removing it from RRule generated string // ics appends "RRULE:" already, so removing it from RRule generated string
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", ""); recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
} }
const partstat: ParticipationStatus = "NEEDS-ACTION"; const partstat: ParticipationStatus = "ACCEPTED";
const role: ParticipationRole = "REQ-PARTICIPANT"; const role: ParticipationRole = "REQ-PARTICIPANT";
const icsEvent = createEvent({ const icsEvent = createEvent({
uid: this.calEvent.iCalUID || this.calEvent.uid!,
start: dayjs(this.calEvent.startTime) start: dayjs(this.calEvent.startTime)
.utc() .utc()
.toArray() .toArray()
.slice(0, 6) .slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc", startInputType: "utc",
productId: "calendso/ics", productId: "calcom/ics",
title: this.calEvent.title, title: this.calEvent.title,
description: this.getTextBody(), description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
attendees: [ attendees: [
...this.calEvent.attendees.map((attendee: Person) => ({ ...this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name, name: attendee.name,
@@ -47,7 +47,7 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
.slice(0, 6) .slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc", startInputType: "utc",
productId: "calendso/ics", productId: "calcom/ics",
title: this.t("ics_event_title", { title: this.t("ics_event_title", {
eventType: this.calEvent.type, eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name, name: this.calEvent.attendees[0].name,
@@ -54,7 +54,7 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu
.slice(0, 6) .slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc", startInputType: "utc",
productId: "calendso/ics", productId: "calcom/ics",
title: this.t("ics_event_title", { title: this.t("ics_event_title", {
eventType: this.calEvent.type, eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name, name: this.calEvent.attendees[0].name,
@@ -35,13 +35,14 @@ export default class OrganizerScheduledEmail extends BaseEmail {
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", ""); recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
} }
const icsEvent = createEvent({ const icsEvent = createEvent({
uid: this.calEvent.iCalUID || this.calEvent.uid!,
start: dayjs(this.calEvent.startTime) start: dayjs(this.calEvent.startTime)
.utc() .utc()
.toArray() .toArray()
.slice(0, 6) .slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc", startInputType: "utc",
productId: "calendso/ics", productId: "calcom/ics",
title: this.calEvent.title, title: this.calEvent.title,
description: this.getTextBody(), description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
Loaded 100 of 226 files, more files were not shown because too many files have changed in this diff. Show more