Compare commits

...
102 Commits
Author SHA1 Message Date
Omar López d7aa3c7163 v2.7.8
release.yml / release (push) Canceled after 0s
2023-04-04 16:58:13 -07:00
Peer Richelsen e899a92071 added turkish (#8091) 2023-04-05 00:13:00 +02:00
zomars ebdbe407cd Fixes crowding update 2023-04-04 12:43:00 -07:00
e7f917bab9 Feedback and improvements (#7903)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-04-04 10:35:51 -07:00
Hariom Balhara ba9688a04a BugFix: Don't open location dialog on removing location (#8085) 2023-04-04 14:43:41 +00:00
Stefan Florea f0e15d77dc feat: Google Tag Manager (GTM) App (#7952) 2023-04-04 17:53:11 +05:30
30b72b687e Follow up insights - user metrics (#8016)
* 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

* text right for table

* Adds missing translation strings

* Fix url state for filter type

* more layout improvements

* more padding improvements

* more UI fixes (padding, sizes) requested by ciaran

* Apply suggestions from code review

* WIP user without team metrics

* yarn

* Adds loading state for graphs and tables

* fix average render when no data

* Multiple fixes in insights

* Fix most and least booked members querys

* More fix for clear filters

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-04-04 13:58:19 +02:00
Peer Richelsenandsean-brydon bbf50dbc53 fixed links and more on signup 404 page that requires license (#7937)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-04-04 11:49:44 +02:00
Ted SpareandPeer Richelsen bb547a0111 Apply labels from linked issues to a PR (#8069)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-04 09:28:36 +00:00
Hariom BalharaandAlex van Andel 1758288948 Improvement: Do immediate redirect for "Redirect on Booking" (#7947)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-04 14:22:27 +05:30
Udit Takkar 11789c725a fix: remove box shadow none (#8077)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-04-04 08:06:11 +00:00
Hariom Balhara 73bfe0aebe HotFix: Broken Email Field in Routing Form (#8074)
* Add darkmode styles and make it full width and other classes that are for TextWidget

* Handle email text change
2023-04-04 07:55:56 +00:00
Harold ThétiotandHariom Balhara 24140f0f29 feat: add sylaps into the app-store in video category (#6728)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-04-04 13:12:07 +05:30
e504f5cd9e Fix/send responses everywhere customInputs were being sent. (#7898)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-04-04 10:29:09 +05:30
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
264 changed files with 7743 additions and 2806 deletions
+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
}
]
}
]
}
@@ -0,0 +1,27 @@
name: "Apply issue labels to PR"
on:
pull_request:
types: [opened]
jobs:
label_on_pr:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 16
- name: Apply labels from linked issue to PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_DATA: ${{ toJson(github.event.pull_request) }}
run: node ./.github/workflows/scripts/apply-issue-labels-to-pr.ts
+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 -1
View File
@@ -26,6 +26,6 @@ jobs:
localization_branch_name: main localization_branch_name: main
create_pull_request: false create_pull_request: false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
@@ -0,0 +1,149 @@
const https = require("https");
async function applyLabelFromLinkedIssueToPR(pr, token) {
// Get the labels from issues linked to the PR
const query = `
query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
closingIssuesReferences(first: 10) {
nodes {
number
labels(first: 10) {
nodes {
name
}
}
}
}
}
}
}
`;
const graphqlData = JSON.stringify({
query,
variables: {
owner: pr.base.repo.owner.login,
repo: pr.base.repo.name,
prNumber: pr.number,
},
});
const requestOptions = {
hostname: "api.github.com",
path: "/graphql",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": graphqlData.length,
"Authorization": "Bearer " + token,
"User-Agent": "Node.js",
},
};
// Use the native Node.js request library to make the request
let linkedIssues;
linkedIssues = await new Promise((resolve, reject) => {
const request = https.request(requestOptions, (response) => {
let responseBody = "";
response.on("data", (chunk) => {
responseBody += chunk;
});
response.on("end", () => {
resolve(
JSON.parse(responseBody)
?.data
?.repository
?.pullRequest
?.closingIssuesReferences
?.nodes
);
});
});
request.on("error", (error) => {
reject(error);
});
request.write(graphqlData);
request.end();
});
if (!linkedIssues || linkedIssues.length === 0) {
console.log("No issue linked.");
return;
}
// Iterate over linked issues and apply labels to PR
for (const issue of linkedIssues) {
const labels = issue?.labels?.nodes?.map((label) => label.name);
if (!labels || labels.length === 0) {
console.log(`No labels found on the linked issue #${issue.number}.`);
continue;
}
const labelsData = JSON.stringify({
labels: labels,
});
const requestOptions = {
hostname: "api.github.com",
path: `/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/issues/${pr.number}/labels`,
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": labelsData.length,
"Authorization": "Bearer " + token,
"User-Agent": "Node.js",
},
};
let labelResult;
labelResult = await new Promise((resolve, reject) => {
const request = https.request(requestOptions, (response) => {
let responseBody = "";
response.on("data", (chunk) => {
responseBody += chunk;
});
response.on("data", () => {});
response.on("end", () => {
resolve(JSON.parse(responseBody));
});
});
request.on("error", (error) => {
reject(error);
});
request.write(labelsData);
request.end();
});
if (labelResult.message) {
console.log(`Error labelling PR: ${labelResult.message}`);
continue;
}
console.log(
`Applied labels: ${labels.join(", ")} to PR #${
pr.number
} from linked issue #${issue.number}`
);
}
}
// Pass the PR data and GitHub token to the script
(async () => {
if (!process.env.PR_DATA) {
console.log("No PR data found.");
return;
}
const prData = JSON.parse(process.env.PR_DATA);
const token = process.env.GITHUB_TOKEN;
await applyLabelFromLinkedIssueToPR(prData, token);
})();
+4 -5
View File
@@ -350,8 +350,7 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
<source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=dark"> <source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=dark">
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light"> <img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
</picture> </picture>
</a> </a>
<!-- CONTRIBUTORS --> <!-- CONTRIBUTORS -->
@@ -456,12 +455,12 @@ following
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one. 1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
2. From within the API console page, go to "Applications". 2. From within the API console page, go to "Applications".
3. Click "ADD CLIENT" button top right and select "Server-based Applications". 3. Click "ADD CLIENT" button top right and select "Server-based Applications".
4. Fill in any information you want in the "Client Details" tab 4. Fill in any information you want in the "Client Details" tab
5. Go to tab "Client Secret" tab. 5. Go to tab "Client Secret" tab.
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields. 6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs. 7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers. 8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
9. Click the "Save"/ "UPDATE" button at the bottom footer. 9. Click the "Save"/ "UPDATE" button at the bottom footer.
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings. 10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
+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;
+74 -87
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,55 +834,43 @@ 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">
<TextField <div className="mr-[9px]">
labelProps={{ className: "hidden" }} <TextField
required labelProps={{ className: "hidden" }}
value={previewState.inline.width} className="focus:ring-offset-0"
onChange={(e) => { required
setPreviewState((previewState) => { value={previewState.inline.width}
const width = e.target.value || "100%"; onChange={(e) => {
setPreviewState((previewState) => {
const width = e.target.value || "100%";
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<>W</>}
/>
</div>
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<>W</>}
/>
<span className="p-2">×</span>
<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", )}
}), </DropdownMenuItem>
dropdownIndicator: (defaultStyles) => ({ ))}
...defaultStyles, </DropdownMenuContent>
display: "none", </Dropdown>
}),
valueContainer: (defaultStyles) => ({
...defaultStyles,
padding: "0",
}),
placeholder: (defaultStyles) => ({
...defaultStyles,
margin: "0",
}),
}}
/>
); );
}} }}
/> />
@@ -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,54 +8,61 @@ 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"> const eventLocationType = getEventLocationType(location.type);
{locations.map((location, index) => { if (!eventLocationType) {
const eventLocationType = getEventLocationType(location.type); // It's possible that the location app got uninstalled
if (!eventLocationType) { return null;
// It's possible that the location app got uninstalled }
return null; if (eventLocationType.variable === "hostDefault") {
} return null;
}
const translateAbleKeys = [ const translateAbleKeys = [
"attendee_in_person", "attendee_in_person",
"in_person", "in_person",
"attendee_phone_number", "attendee_phone_number",
"link_meeting", "link_meeting",
"organizer_phone_number", "organizer_phone_number",
]; ];
const locationKey = z.string().default("").parse(locationKeyToString(location)); const locationKey = z.string().default("").parse(locationKeyToString(location));
const translatedLocation = location.type.startsWith("integrations:")
? eventLocationType.label
: translateAbleKeys.includes(locationKey)
? t(locationKey)
: locationKey;
const translatedLocation = location.type.startsWith("integrations:") return (
? eventLocationType.label <div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
: translateAbleKeys.includes(locationKey) {eventLocationType.iconUrl === "/link.svg" ? (
? t(locationKey) <FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
: locationKey; ) : (
<img
return ( src={eventLocationType.iconUrl}
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium"> className={classNames(
{eventLocationType.iconUrl === "/link.svg" ? ( "ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " /> !eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
) : (
<img
src={eventLocationType.iconUrl}
className={classNames(
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
)}
alt={`${eventLocationType.label} icon`}
/>
)} )}
<Tooltip content={translatedLocation}> alt={`${eventLocationType.label} icon`}
<p className="line-clamp-1">{translatedLocation}</p> />
</Tooltip> )}
</div> <Tooltip content={translatedLocation}>
); <p className="line-clamp-1">{translatedLocation}</p>
})} </Tooltip>
</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 <>
value={field.value} <AvailabilitySelect
onBlur={field.onBlur} value={field.value}
name={field.name} onBlur={field.onBlur}
onChange={(selected) => { name={field.name}
field.onChange(selected?.value || null); schedules={schedules?.schedules || []}
}} isLoading={isLoading}
/> onChange={(selected) => {
field.onChange(selected?.value || null);
}}
/>
</>
)} )}
/> />
</div> </div>
@@ -10,11 +10,12 @@ import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } from "@calcom/core/event"; import { getEventName } from "@calcom/core/event";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector"; import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder"; import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants"; import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client"; import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui"; import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
import { FiEdit, FiCopy } from "@calcom/ui/components/icon"; import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController"; import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -183,9 +184,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
setRedirectUrlVisible(e); setRedirectUrlVisible(e);
onChange(e ? value : ""); onChange(e ? value : "");
}}> }}>
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */} {/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
<div className="lg:-ml-2"> <div className="lg:-ml-2 lg:-mb-2">
<TextField <TextField
className="w-full"
label={t("redirect_success_booking")} label={t("redirect_success_booking")}
labelSrOnly labelSrOnly
placeholder={t("external_redirect_url")} placeholder={t("external_redirect_url")}
@@ -194,21 +196,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
defaultValue={eventType.successRedirectUrl || ""} defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")} {...formMethods.register("successRedirectUrl")}
/> />
<div className="mt-2 flex"> <div
<Checkbox className={classNames(
description={t("disable_success_page")} "p-1 text-sm text-orange-600",
// Disable if it's not Self Hosted or if the redirect url is not set formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")} )}>
{...formMethods.register("metadata.disableSuccessPage")} {t("redirect_url_warning")}
/>
{/*TODO: Extract it out into a component when used more than once*/}
{!IS_SELF_HOSTED && (
<Link href="https://cal.com/pricing" target="_blank">
<Badge variant="orange" className="ml-2">
Platform Only
</Badge>
</Link>
)}
</div> </div>
</div> </div>
</SettingsToggle> </SettingsToggle>
@@ -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}
+44 -25
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 }),
@@ -198,14 +229,6 @@ export const EventSetupTab = (
return ( return (
<li <li
onClick={() => {
locationFormMethods.setValue("locationType", location.type);
locationFormMethods.unregister("locationLink");
locationFormMethods.unregister("locationAddress");
locationFormMethods.unregister("locationPhoneNumber");
setEditingLocationType(location.type);
openLocationModal(location.type);
}}
key={`${location.type}${index}`} key={`${location.type}${index}`}
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer"> className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -287,12 +310,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 +427,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">
<FiArrowLeft className="mr-1" /> {t("adding_events_to")} <div className="flex">
<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} />
+10 -1
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">
<MetaProvider>{props.children}</MetaProvider> <FeatureFlagsProvider>
<MetaProvider>{props.children}</MetaProvider>
</FeatureFlagsProvider>
</ThemeProvider> </ThemeProvider>
</TooltipProvider> </TooltipProvider>
</CustomI18nextProvider> </CustomI18nextProvider>
+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.8",
"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",
+42 -5
View File
@@ -3,10 +3,10 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants"; import { 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 (
<> <>
@@ -62,7 +99,7 @@ export default function Custom404() {
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900"> <h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900">
{t("signup_requires")} {t("signup_requires")}
</h1> </h1>
<p className="mt-4">{t("signup_requires_description", { companyName: COMPANY_NAME })}</p> <p className="mt-4">{t("signup_requires_description", { companyName: "Cal.com" })}</p>
</div> </div>
<div className="mt-12"> <div className="mt-12">
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500"> <h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
@@ -99,7 +136,7 @@ export default function Custom404() {
<ul role="list" className="divide-y divide-gray-200 border-gray-200"> <ul role="list" className="divide-y divide-gray-200 border-gray-200">
<li className="px-4 py-2"> <li className="px-4 py-2">
<Link <Link
href={`${DEVELOPER_DOCS}/self-hosting/installation`} href="https://cal.com/self-hosting/installation"
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50"> <span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
@@ -168,7 +205,7 @@ export default function Custom404() {
</ul> </ul>
<div className="mt-8"> <div className="mt-8">
<Link <Link
href={`${WEBSITE_URL}/enterprise`} href="https://cal.com/sales"
className="text-base font-medium text-black hover:text-gray-500"> className="text-base font-medium text-black hover:text-gray-500">
{t("contact_sales")} {t("contact_sales")}
<span aria-hidden="true"> &rarr;</span> <span aria-hidden="true"> &rarr;</span>
+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 =
+11 -8
View File
@@ -13,7 +13,10 @@ import {
getGroupName, getGroupName,
getUsernameList, getUsernameList,
} from "@calcom/lib/defaultEvents"; } from "@calcom/lib/defaultEvents";
import getBooking from "@calcom/lib/getBooking";
import type { GetBookingType } from "@calcom/lib/getBooking";
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,
@@ -21,8 +24,6 @@ import {
userMetadata as userMetadataSchema, userMetadata as userMetadataSchema,
} from "@calcom/prisma/zod-utils"; } from "@calcom/prisma/zod-utils";
import type { GetBookingType } from "@lib/getBooking";
import getBooking from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage"; import BookingPage from "@components/booking/pages/BookingPage";
@@ -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,
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { sendOrganizerRequestReminderEmail } from "@calcom/emails"; import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getTranslation } from "@calcom/lib/server/i18n"; import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import prisma, { bookingMinimalSelect } from "@calcom/prisma";
@@ -48,8 +49,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
eventType: { eventType: {
select: { select: {
recurringEvent: true, recurringEvent: true,
bookingFields: true,
}, },
}, },
responses: true,
uid: true, uid: true,
destinationCalendar: true, destinationCalendar: true,
}, },
@@ -96,6 +99,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
title: booking.title, title: booking.title,
description: booking.description || undefined, description: booking.description || undefined,
customInputs: isPrismaObjOrUndefined(booking.customInputs), customInputs: isPrismaObjOrUndefined(booking.customInputs),
...getCalEventResponses({
bookingFields: booking.eventType?.bookingFields ?? null,
booking,
}),
location: booking.location ?? "", location: booking.location ?? "",
startTime: booking.startTime.toISOString(), startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(), endTime: booking.endTime.toISOString(),
+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>
+23 -141
View File
@@ -6,7 +6,7 @@ import type { GetServerSidePropsContext } from "next";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { RRule } from "rrule"; import { RRule } from "rrule";
import { z } from "zod"; import { z } from "zod";
@@ -36,6 +36,7 @@ import {
formatToLocalizedTimezone, formatToLocalizedTimezone,
} from "@calcom/lib/date-fns"; } from "@calcom/lib/date-fns";
import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { getBookingWithResponses } from "@calcom/lib/getBooking";
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 { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
@@ -51,7 +52,6 @@ import { Button, EmailInput, HeadSeo, Badge } from "@calcom/ui";
import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon"; import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock"; import { timeZone } from "@lib/clock";
import { getBookingWithResponses } from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import CancelBooking from "@components/booking/CancelBooking"; import CancelBooking from "@components/booking/CancelBooking";
@@ -59,114 +59,6 @@ import EventReservationSchema from "@components/schemas/EventReservationSchema";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
function redirectToExternalUrl(url: string) {
window.parent.location.href = url;
}
/**
* Redirects to external URL with query params from current URL.
* Query Params and Hash Fragment if present in external URL are kept intact.
*/
function RedirectionToast({ url }: { url: string }) {
const [timeRemaining, setTimeRemaining] = useState(10);
const [isToastVisible, setIsToastVisible] = useState(true);
const { t } = useLocale();
const timerRef = useRef<number | null>(null);
const router = useRouter();
const { cancel: isCancellationMode } = querySchema.parse(router.query);
const urlWithSuccessParamsRef = useRef<string | null>();
if (isCancellationMode && timerRef.current) {
setIsToastVisible(false);
}
useEffect(() => {
if (!isToastVisible && timerRef.current) {
window.clearInterval(timerRef.current);
}
}, [isToastVisible]);
useEffect(() => {
const parsedExternalUrl = new URL(url);
const parsedSuccessUrl = new URL(document.URL);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */ //https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type
for (const [name, value] of parsedExternalUrl.searchParams.entries()) {
parsedSuccessUrl.searchParams.set(name, value);
}
const urlWithSuccessParams =
parsedExternalUrl.origin +
parsedExternalUrl.pathname +
"?" +
parsedSuccessUrl.searchParams.toString() +
parsedExternalUrl.hash;
urlWithSuccessParamsRef.current = urlWithSuccessParams;
timerRef.current = window.setInterval(() => {
if (timeRemaining > 0) {
setTimeRemaining((timeRemaining) => {
return timeRemaining - 1;
});
} else {
redirectToExternalUrl(urlWithSuccessParams);
window.clearInterval(timerRef.current as number);
}
}, 1000);
return () => {
window.clearInterval(timerRef.current as number);
};
}, [timeRemaining, url]);
if (!isToastVisible) {
return null;
}
return (
<>
<div className="relative z-[60] pb-2 sm:pb-5">
<div className="mx-auto w-full sm:max-w-7xl sm:px-2 lg:px-8">
<div className="border border-green-600 bg-green-500 p-2 sm:p-3">
<div className="flex flex-wrap items-center justify-between">
<div className="flex w-0 flex-1 items-center">
<p className="truncate font-medium text-white sm:mx-3">
<span className="md:hidden">Redirecting to {url} ...</span>
<span className="hidden md:inline">
{t("you_are_being_redirected", { url, seconds: timeRemaining })}
</span>
</p>
</div>
<div className="order-3 mt-2 w-full flex-shrink-0 sm:order-2 sm:mt-0 sm:w-auto">
<button
onClick={() => {
if (urlWithSuccessParamsRef.current) {
redirectToExternalUrl(urlWithSuccessParamsRef.current);
}
}}
className="flex w-full items-center justify-center rounded-sm border border-transparent bg-white px-4 py-2 text-sm font-medium text-green-600 hover:bg-green-50">
{t("continue")}
</button>
</div>
<div className="order-2 flex-shrink-0 sm:order-3 sm:ml-2">
<button
type="button"
onClick={() => {
setIsToastVisible(false);
}}
className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white">
<FiX className="h-6 w-6 text-white" />
</button>
</div>
</div>
</div>
</div>
</div>
</>
);
}
type SuccessProps = inferSSRProps<typeof getServerSideProps>; type SuccessProps = inferSSRProps<typeof getServerSideProps>;
const stringToBoolean = z const stringToBoolean = z
@@ -176,13 +68,14 @@ const stringToBoolean = z
const querySchema = z.object({ const querySchema = z.object({
uid: z.string(), uid: z.string(),
allRemainingBookings: stringToBoolean, email: z.string().optional(),
eventTypeSlug: z.string().optional(),
cancel: stringToBoolean, cancel: stringToBoolean,
allRemainingBookings: 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(),
seatReferenceUid: z.string().optional(), seatReferenceUid: z.string().optional(),
}); });
@@ -266,7 +159,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 =
@@ -400,9 +297,6 @@ export default function Success(props: SuccessProps) {
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} /> <CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}> <main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}> <div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
{isSuccessBookingPage && !isCancellationMode && eventType.successRedirectUrl ? (
<RedirectionToast url={eventType.successRedirectUrl} />
) : null}{" "}
<div <div
className={classNames( className={classNames(
shouldAlignCentrally ? "text-center" : "", shouldAlignCentrally ? "text-center" : "",
@@ -485,7 +379,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 +421,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 +459,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 +475,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 {response.toString()}
className="break-words" </p>
data-testid="field-response"
data-fob-field={field.name}>
{response.toString()}
</p>
</div>
</> </>
); );
})} })}
@@ -981,13 +871,6 @@ const getEventTypesFromDB = async (id: number) => {
}; };
}; };
const schema = z.object({
uid: z.string(),
email: z.string().optional(),
eventTypeSlug: z.string().optional(),
cancel: stringToBoolean,
});
const handleSeatsEventTypeOnBooking = ( const handleSeatsEventTypeOnBooking = (
eventType: { eventType: {
seatsPerTimeSlot?: number | null; seatsPerTimeSlot?: number | null;
@@ -1017,9 +900,9 @@ const handleSeatsEventTypeOnBooking = (
export async function getServerSideProps(context: GetServerSidePropsContext) { export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context); const ssr = await ssrInit(context);
const parsedQuery = schema.safeParse(context.query); const parsedQuery = querySchema.safeParse(context.query);
if (!parsedQuery.success) return { notFound: true }; if (!parsedQuery.success) return { notFound: true };
const { uid, email, eventTypeSlug, cancel } = parsedQuery.data; const { uid, email, eventTypeSlug, cancel, isSuccessBookingPage } = parsedQuery.data;
const bookingInfoRaw = await prisma.booking.findFirst({ const bookingInfoRaw = await prisma.booking.findFirst({
where: { where: {
@@ -1087,8 +970,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;
@@ -1117,7 +999,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs), customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
}; };
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl && !cancel) { if (eventType.successRedirectUrl && isSuccessBookingPage && !cancel) {
return { return {
redirect: { redirect: {
destination: eventType.successRedirectUrl, destination: eventType.successRedirectUrl,
+3 -1
View File
@@ -5,11 +5,12 @@ 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 type { GetBookingType } from "@calcom/lib/getBooking";
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";
import type { GetBookingType } from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr"; import type { EmbedProps } from "@lib/withEmbedSsr";
@@ -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,
+11 -25
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 {
props: {
return { type: typeParam,
props: { trpcState: ssr.dehydrate(),
// session, },
type: typeParam, };
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>
+128
View File
@@ -0,0 +1,128 @@
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 { 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();
return (
<div className="min-w-52 hidden md:block">
<h3 className="font-cal max-w-28 sm:max-w-72 md:max-w-80 truncate text-xl font-semibold tracking-wide text-black xl:max-w-full">
{t("analytics_for_organisation")}
</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-8 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_insights")}
<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;
+5 -5
View File
@@ -2,15 +2,15 @@ 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 getBooking from "@calcom/lib/getBooking";
import type { GetBookingType } from "@calcom/lib/getBooking";
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";
import { asStringOrNull } from "@lib/asStringOrNull"; import { asStringOrNull } from "@lib/asStringOrNull";
import type { GetBookingType } from "@lib/getBooking";
import getBooking from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr"; import type { EmbedProps } from "@lib/withEmbedSsr";
@@ -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;
+6 -3
View File
@@ -5,12 +5,13 @@ 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 type { GetBookingType } from "@calcom/lib/getBooking";
import getBooking from "@calcom/lib/getBooking";
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";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull"; import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import type { GetBookingType } from "@lib/getBooking";
import getBooking from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage"; import BookingPage from "@components/booking/pages/BookingPage";
@@ -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 }) => {
+3 -1
View File
@@ -4,4 +4,6 @@ Disallow: /api
Disallow: /static/locales 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

+47 -11
View File
@@ -111,7 +111,7 @@
"hidden_team_owner_message": "You need a pro account to use teams, you are hidden until you upgrade.", "hidden_team_owner_message": "You need a pro account to use teams, you are hidden until you upgrade.",
"link_expires": "p.s. It expires in {{expiresIn}} hours.", "link_expires": "p.s. It expires in {{expiresIn}} hours.",
"upgrade_to_per_seat": "Upgrade to Per-Seat", "upgrade_to_per_seat": "Upgrade to Per-Seat",
"seat_options_doesnt_support_confirmation":"Seats option doesn't support confirmation requirement", "seat_options_doesnt_support_confirmation": "Seats option doesn't support confirmation requirement",
"team_upgrade_seats_details": "Of the {{memberCount}} members in your team, {{unpaidCount}} seat(s) are unpaid. At ${{seatPrice}}/month per seat the estimated total cost of your membership is ${{totalCost}}/month.", "team_upgrade_seats_details": "Of the {{memberCount}} members in your team, {{unpaidCount}} seat(s) are unpaid. At ${{seatPrice}}/month per seat the estimated total cost of your membership is ${{totalCost}}/month.",
"team_upgrade_banner_description": "Thank you for trialing our new team plan. We noticed your team \"{{teamName}}\" needs to be upgraded.", "team_upgrade_banner_description": "Thank you for trialing our new team plan. We noticed your team \"{{teamName}}\" needs to be upgraded.",
"team_upgrade_banner_action": "Upgrade here", "team_upgrade_banner_action": "Upgrade here",
@@ -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.",
@@ -433,7 +434,7 @@
"password_hint_min": "Minimum 7 characters long", "password_hint_min": "Minimum 7 characters long",
"password_hint_admin_min": "Minimum 15 characters long", "password_hint_admin_min": "Minimum 15 characters long",
"password_hint_num": "Contain at least 1 number", "password_hint_num": "Contain at least 1 number",
"max_limit_allowed_hint":"Must be {{limit}} or fewer characters long", "max_limit_allowed_hint": "Must be {{limit}} or fewer characters long",
"invalid_password_hint": "The password must be a minimum of {{passwordLength}} characters long containing at least one number and have a mixture of uppercase and lowercase letters", "invalid_password_hint": "The password must be a minimum of {{passwordLength}} characters long containing at least one number and have a mixture of uppercase and lowercase letters",
"incorrect_password": "Password is incorrect.", "incorrect_password": "Password is incorrect.",
"incorrect_username_password": "Username or password is incorrect.", "incorrect_username_password": "Username or password is incorrect.",
@@ -863,7 +864,7 @@
"add_new_calendar": "Add a new calendar", "add_new_calendar": "Add a new calendar",
"set_calendar": "Set where to add new events to when you're booked.", "set_calendar": "Set where to add new events to when you're booked.",
"delete_schedule": "Delete schedule", "delete_schedule": "Delete schedule",
"delete_schedule_description":"Deleting a schedule will remove it from all event types. This action cannot be undone.", "delete_schedule_description": "Deleting a schedule will remove it from all event types. This action cannot be undone.",
"schedule_created_successfully": "{{scheduleName}} schedule created successfully", "schedule_created_successfully": "{{scheduleName}} schedule created successfully",
"availability_updated_successfully": "{{scheduleName}} schedule updated successfully", "availability_updated_successfully": "{{scheduleName}} schedule updated successfully",
"schedule_deleted_successfully": "Schedule deleted successfully", "schedule_deleted_successfully": "Schedule deleted successfully",
@@ -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",
@@ -1631,22 +1634,30 @@
"a_routing_form": "A Routing Form", "a_routing_form": "A Routing Form",
"form_description_placeholder": "Form Description", "form_description_placeholder": "Form Description",
"keep_me_connected_with_form": "Keep me connected with the form", "keep_me_connected_with_form": "Keep me connected with the form",
"fields_in_form_duplicated":"Any changes in Router and Fields of the form being duplicated, would reflect in the duplicate.", "fields_in_form_duplicated": "Any changes in Router and Fields of the form being duplicated, would reflect in the duplicate.",
"form_deleted": "Form deleted", "form_deleted": "Form deleted",
"delete_form": "Are you sure you want to delete this form?", "delete_form": "Are you sure you want to delete this form?",
"delete_form_action": "Yes, delete Form", "delete_form_action": "Yes, delete Form",
"delete_form_confirmation":"Anyone who you've shared the link with will no longer be able to access it.", "delete_form_confirmation": "Anyone who you've shared the link with will no longer be able to access it.",
"delete_form_confirmation_2":"All associated responses will be deleted.", "delete_form_confirmation_2": "All associated responses will be deleted.",
"typeform_redirect_url_copied": "Typeform Redirect URL copied! You can go and set the URL in Typeform form.", "typeform_redirect_url_copied": "Typeform Redirect URL copied! You can go and set the URL in Typeform form.",
"modifications_in_fields_warning": "Modifications in fields and routes of following forms will be reflected in this form.", "modifications_in_fields_warning": "Modifications in fields and routes of following forms will be reflected in this form.",
"connected_forms": "Connected Forms", "connected_forms": "Connected Forms",
"form_modifications_warning": "Following forms would be affected when you modify fields or routes here.", "form_modifications_warning": "Following forms would be affected when you modify fields or routes here.",
"responses_collection_waiting_description": "Wait for some time for responses to be collected. You can go and submit the form yourself as well.", "responses_collection_waiting_description": "Wait for some time for responses to be collected. You can go and submit the form yourself as well.",
"this_is_what_your_users_would_see":"This is what your users would see", "this_is_what_your_users_would_see": "This is what your users would see",
"identifies_name_field": "Identifies field by this name.", "identifies_name_field": "Identifies field by this name.",
"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",
@@ -1657,8 +1668,33 @@
"verification_code": "Verification code", "verification_code": "Verification code",
"can_you_try_again": "Can you try again with a different time?", "can_you_try_again": "Can you try again with a different time?",
"verify": "Verify", "verify": "Verify",
"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_insights": "Looking for more Insights?",
"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",
"subtitle_analytics": "Learn more about your team's activity",
"redirect_url_warning": "Adding a redirect will disable the success page. Make sure to mention \"Booking Confirmed\" on your custom success page.",
"event_trends": "Event Trends",
"clear_filters": "Clear Filters",
"insights": "Insights",
"insights_no_data_found_for_filter": "No data found for the selected filter or selected dates.",
"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 -2
View File
@@ -148,8 +148,6 @@ select:focus {
--tw-ring-color: var(--brand-color); --tw-ring-color: var(--brand-color);
border-color: var(--brand-color); border-color: var(--brand-color);
} }
/* https://stackoverflow.com/a/69308190 */
input[type='text']:focus { box-shadow: none; }
@layer components { @layer components {
@@ -497,3 +495,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 }) {
@@ -40,6 +40,7 @@ export const EventTypeAddonMap = {
fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")), fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")),
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")), ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")), giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
"google-tag-manager": dynamic(() => import("./google-tag-manager/components/EventTypeAppCardInterface")),
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")), plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")), qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")),
rainbow: dynamic(() => import("./rainbow/components/EventTypeAppCardInterface")), rainbow: dynamic(() => import("./rainbow/components/EventTypeAppCardInterface")),
@@ -6,6 +6,7 @@ import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
import { appKeysSchema as fathom_zod_ts } from "./fathom/zod"; import { appKeysSchema as fathom_zod_ts } from "./fathom/zod";
import { appKeysSchema as ga4_zod_ts } from "./ga4/zod"; import { appKeysSchema as ga4_zod_ts } from "./ga4/zod";
import { appKeysSchema as giphy_zod_ts } from "./giphy/zod"; import { appKeysSchema as giphy_zod_ts } from "./giphy/zod";
import { appKeysSchema as google_tag_manager_zod_ts } from "./google-tag-manager/zod";
import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod"; import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod"; import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod";
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
@@ -30,6 +31,7 @@ export const appKeysSchemas = {
fathom: fathom_zod_ts, fathom: fathom_zod_ts,
ga4: ga4_zod_ts, ga4: ga4_zod_ts,
giphy: giphy_zod_ts, giphy: giphy_zod_ts,
"google-tag-manager": google_tag_manager_zod_ts,
googlecalendar: googlecalendar_zod_ts, googlecalendar: googlecalendar_zod_ts,
hubspot: hubspot_zod_ts, hubspot: hubspot_zod_ts,
larkcalendar: larkcalendar_zod_ts, larkcalendar: larkcalendar_zod_ts,
@@ -17,6 +17,7 @@ import facetime_config_json from "./facetime/config.json";
import fathom_config_json from "./fathom/config.json"; import fathom_config_json from "./fathom/config.json";
import ga4_config_json from "./ga4/config.json"; import ga4_config_json from "./ga4/config.json";
import { metadata as giphy__metadata_ts } from "./giphy/_metadata"; import { metadata as giphy__metadata_ts } from "./giphy/_metadata";
import google_tag_manager_config_json from "./google-tag-manager/config.json";
import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata"; import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata";
import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata"; import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata";
import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata"; import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata";
@@ -39,6 +40,7 @@ import sendgrid_config_json from "./sendgrid/config.json";
import signal_config_json from "./signal/config.json"; import signal_config_json from "./signal/config.json";
import sirius_video_config_json from "./sirius_video/config.json"; import sirius_video_config_json from "./sirius_video/config.json";
import { metadata as stripepayment__metadata_ts } from "./stripepayment/_metadata"; import { metadata as stripepayment__metadata_ts } from "./stripepayment/_metadata";
import sylapsvideo_config_json from "./sylapsvideo/config.json";
import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata"; import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata";
import telegram_config_json from "./telegram/config.json"; import telegram_config_json from "./telegram/config.json";
import basic_config_json from "./templates/basic/config.json"; import basic_config_json from "./templates/basic/config.json";
@@ -75,6 +77,7 @@ export const appStoreMetadata = {
fathom: fathom_config_json, fathom: fathom_config_json,
ga4: ga4_config_json, ga4: ga4_config_json,
giphy: giphy__metadata_ts, giphy: giphy__metadata_ts,
"google-tag-manager": google_tag_manager_config_json,
googlecalendar: googlecalendar__metadata_ts, googlecalendar: googlecalendar__metadata_ts,
googlevideo: googlevideo__metadata_ts, googlevideo: googlevideo__metadata_ts,
hubspot: hubspot__metadata_ts, hubspot: hubspot__metadata_ts,
@@ -97,6 +100,7 @@ export const appStoreMetadata = {
signal: signal_config_json, signal: signal_config_json,
sirius_video: sirius_video_config_json, sirius_video: sirius_video_config_json,
stripepayment: stripepayment__metadata_ts, stripepayment: stripepayment__metadata_ts,
sylapsvideo: sylapsvideo_config_json,
tandemvideo: tandemvideo__metadata_ts, tandemvideo: tandemvideo__metadata_ts,
telegram: telegram_config_json, telegram: telegram_config_json,
basic: basic_config_json, basic: basic_config_json,
@@ -6,6 +6,7 @@ import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
import { appDataSchema as fathom_zod_ts } from "./fathom/zod"; import { appDataSchema as fathom_zod_ts } from "./fathom/zod";
import { appDataSchema as ga4_zod_ts } from "./ga4/zod"; import { appDataSchema as ga4_zod_ts } from "./ga4/zod";
import { appDataSchema as giphy_zod_ts } from "./giphy/zod"; import { appDataSchema as giphy_zod_ts } from "./giphy/zod";
import { appDataSchema as google_tag_manager_zod_ts } from "./google-tag-manager/zod";
import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod"; import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod"; import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod";
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
@@ -30,6 +31,7 @@ export const appDataSchemas = {
fathom: fathom_zod_ts, fathom: fathom_zod_ts,
ga4: ga4_zod_ts, ga4: ga4_zod_ts,
giphy: giphy_zod_ts, giphy: giphy_zod_ts,
"google-tag-manager": google_tag_manager_zod_ts,
googlecalendar: googlecalendar_zod_ts, googlecalendar: googlecalendar_zod_ts,
hubspot: hubspot_zod_ts, hubspot: hubspot_zod_ts,
larkcalendar: larkcalendar_zod_ts, larkcalendar: larkcalendar_zod_ts,
@@ -17,6 +17,7 @@ export const apiHandlers = {
fathom: import("./fathom/api"), fathom: import("./fathom/api"),
ga4: import("./ga4/api"), ga4: import("./ga4/api"),
giphy: import("./giphy/api"), giphy: import("./giphy/api"),
"google-tag-manager": import("./google-tag-manager/api"),
googlecalendar: import("./googlecalendar/api"), googlecalendar: import("./googlecalendar/api"),
googlevideo: import("./googlevideo/api"), googlevideo: import("./googlevideo/api"),
hubspot: import("./hubspot/api"), hubspot: import("./hubspot/api"),
@@ -39,6 +40,7 @@ export const apiHandlers = {
signal: import("./signal/api"), signal: import("./signal/api"),
sirius_video: import("./sirius_video/api"), sirius_video: import("./sirius_video/api"),
stripepayment: import("./stripepayment/api"), stripepayment: import("./stripepayment/api"),
sylapsvideo: import("./sylapsvideo/api"),
tandemvideo: import("./tandemvideo/api"), tandemvideo: import("./tandemvideo/api"),
telegram: import("./telegram/api"), telegram: import("./telegram/api"),
basic: import("./templates/basic/api"), basic: import("./templates/basic/api"),
+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 -->
@@ -0,0 +1,16 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
createCredential: ({ appType, user, slug }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
};
export default handler;
@@ -0,0 +1 @@
export { default as add } from "./add";
@@ -0,0 +1,38 @@
import { useState } from "react";
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import AppCard from "@calcom/app-store/_components/AppCard";
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
import { TextField } from "@calcom/ui";
import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
const trackingId = getAppData("trackingId");
const [enabled, setEnabled] = useState(getAppData("enabled"));
return (
<AppCard
setAppData={setAppData}
app={app}
switchOnClick={(e) => {
if (!e) {
setEnabled(false);
} else {
setEnabled(true);
}
}}
switchChecked={enabled}>
<TextField
name="Tracking ID"
value={trackingId}
onChange={(e) => {
setAppData("trackingId", e.target.value);
}}
/>
</AppCard>
);
};
export default EventTypeAppCard;
@@ -0,0 +1,28 @@
{
"name": "Google Tag Manager",
"slug": "google-tag-manager",
"type": "google-tag-manager_analytics",
"imageSrc": "icon.svg",
"logo": "icon.svg",
"url": "https://tagmanager.google.com",
"variant": "analytics",
"categories": [
"analytics"
],
"publisher": "Black Lemon",
"email": "support@blacklemon.dk",
"description": "App to install Google Tag Manager",
"extendsFeature": "EventType",
"appData": {
"tag": {
"scripts": [
{
"content":"(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-{TRACKING_ID}');"
}
]
}
},
"isTemplate": false,
"__createdUsingCli": true,
"__template": "booking-pages-tag"
}
@@ -0,0 +1 @@
export * as api from "./api";
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/google-tag-manager",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "App to install Google Tag Manager"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.87 122.89"><defs><style>.cls-1{fill:#8ab4f8;}.cls-2{fill:#4285f4;}.cls-3{fill:#246fdb;}</style></defs><title>google-tag-manager</title><polygon class="cls-1" points="72.13 118.22 50.8 96.86 96.63 50.56 118.35 72.28 72.13 118.22 72.13 118.22"/><path class="cls-2" d="M72.29,26.23,50.56,4.5,4.5,50.56a15.35,15.35,0,0,0,0,21.71h0l46.06,46.06,21.31-21.4L37.07,61.45,72.29,26.23Z"/><path class="cls-1" d="M118.35,50.56,72.29,4.5A15.36,15.36,0,0,0,50.56,26.22L96.65,72.28a15.36,15.36,0,0,0,21.72-21.72Z"/><path class="cls-3" d="M76.48,107.66a15.23,15.23,0,1,0-15.22,15.23,15.22,15.22,0,0,0,15.22-15.23Z"/></svg>

After

Width:  |  Height:  |  Size: 695 B

@@ -0,0 +1,11 @@
import { z } from "zod";
import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod";
export const appDataSchema = eventTypeAppCardZod.merge(
z.object({
trackingId: z.string(),
})
);
export const appKeysSchema = z.object({});
@@ -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) {
+2
View File
@@ -20,6 +20,7 @@ import * as plausible from "./plausible";
import * as salesforce from "./salesforce"; import * as salesforce from "./salesforce";
import * as sendgrid from "./sendgrid"; import * as sendgrid from "./sendgrid";
import * as stripepayment from "./stripepayment"; import * as stripepayment from "./stripepayment";
import * as sylapsvideo from "./sylapsvideo";
import * as tandemvideo from "./tandemvideo"; import * as tandemvideo from "./tandemvideo";
import * as vital from "./vital"; import * as vital from "./vital";
import * as wipemycalother from "./wipemycalother"; import * as wipemycalother from "./wipemycalother";
@@ -38,6 +39,7 @@ const appStore = {
hubspot, hubspot,
huddle01video, huddle01video,
jitsivideo, jitsivideo,
sylapsvideo,
larkcalendar, larkcalendar,
office365calendar, office365calendar,
office365video, office365video,
+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"

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