Compare commits

...
65 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
201 changed files with 4893 additions and 2888 deletions
+6 -6
View File
@@ -12,13 +12,13 @@ Fixes # (issue)
## 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)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- Bug fix (non-breaking change which fixes an issue)
- Chore (refactoring code, technical debt, workflow improvements)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
- This change requires a documentation update
## 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: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- name: Show info
run: node -e "console.log(require('v8').getHeapStatistics())"
- run: yarn type-check
- name: Configure TSC problem matcher
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
on:
workflow_dispatch:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
# Runs “Every month at 1st (see https://crontab.guru)
- cron: "0 0 1 * *"
jobs:
cron-downgradeUsers:
env:
+1 -1
View File
@@ -26,6 +26,6 @@ jobs:
localization_branch_name: main
create_pull_request: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
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">
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
</picture>
</a>
</a>
<!-- 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.
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
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.
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.
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:report": "eslint . --format json --output-file ../../lint-results/docs.json",
"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.",
"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 type { NextRouter } from "next/router";
import { useRouter } from "next/router";
@@ -22,7 +22,7 @@ import {
TextField,
ColorPicker,
} 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";
@@ -167,7 +167,7 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
const height = getDimension(previewState.inline.height);
return code`
import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function MyComponent() {
useEffect(()=>{
(async function () {
@@ -187,12 +187,12 @@ function MyComponent() {
}) => {
return code`
import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function MyComponent() {
useEffect(()=>{
(async function () {
const cal = await getCalApi();
Cal("floatingButton", ${floatingButtonArg});
cal("floatingButton", ${floatingButtonArg});
${uiInstructionCode}
})();
}, [])
@@ -201,7 +201,7 @@ function MyComponent() {
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
return code`
import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function MyComponent() {
useEffect(()=>{
(async function () {
@@ -313,7 +313,7 @@ const embeds: {
}[] = [
{
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",
illustration: (
<svg
@@ -798,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
}
const ThemeOptions = [
{ value: Theme.auto, label: "Auto Theme" },
{ value: Theme.auto, label: "Auto" },
{ value: Theme.dark, label: "Dark Theme" },
{ value: Theme.light, label: "Light Theme" },
];
@@ -815,11 +815,18 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
];
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 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
className="h-6 w-6"
onClick={() => {
removeQueryParams(router, ["embedType", "embedTabName"]);
}}>
@@ -827,55 +834,43 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</button>
{embed.title}
</h3>
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
<div className="flex flex-col overflow-y-auto">
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
<h4 className="mb-6 text-sm font-normal text-gray-500">{embed.subtitle}</h4>
<div className="flex flex-col">
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible
open={isEmbedCustomizationOpen}
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">
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-sm">Embed Window Sizing</div>
<div className="justify-left flex items-center">
<TextField
labelProps={{ className: "hidden" }}
required
value={previewState.inline.width}
onChange={(e) => {
setPreviewState((previewState) => {
const width = e.target.value || "100%";
<div className="mb-[9px] text-sm">Window sizing</div>
<div className="justify-left flex items-center !font-normal">
<div className="mr-[9px]">
<TextField
labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
required
value={previewState.inline.width}
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
labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
value={previewState.inline.height}
required
onChange={(e) => {
@@ -1007,22 +1002,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</CollapsibleContent>
</Collapsible>
</div>
<hr className="mt-4" />
<div className="mt-4 font-medium">
<div className="font-medium">
<Collapsible
open={isBookingCustomizationOpen}
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>
<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
checked={previewState.hideEventTypeDetails}
onCheckedChange={(checked) => {
@@ -1036,13 +1022,38 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
/>
<div className="text-sm">{t("hide_eventtype_details")}</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>
<Select
className="w-full"
defaultValue={ThemeOptions[0]}
components={{
Control: ThemeSelectControl,
IndicatorSeparator: () => null,
}}
onChange={(option) => {
if (!option) {
@@ -1058,30 +1069,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
options={ThemeOptions}
/>
</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>
</CollapsibleContent>
</Collapsible>
@@ -1,10 +1,14 @@
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, Select } from "@calcom/ui";
import {
Button,
Dropdown,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
DropdownItem,
} from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -13,31 +17,6 @@ interface AdditionalCalendarSelectorProps {
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 { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
@@ -59,51 +38,39 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
type: "new_other",
});
return (
<Select
name="additionalCalendar"
placeholder={
<Button StartIcon={FiPlus} color="secondary">
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon={FiPlus} color="secondary" {...(isLoading && { loading: isLoading })}>
{t("add")}
</Button>
}
options={options}
isSearchable={false}
isLoading={isLoading}
components={{ Option: ImageOption }}
styles={{
menu: (defaultStyles) => ({
...defaultStyles,
"@media only screen and (max-width: 640px)": {
left: "0",
},
width: "max-content",
right: "0",
}),
indicatorSeparator: (defaultStyles) => ({
...defaultStyles,
display: "none",
}),
control: (defaultStyles) => ({
...defaultStyles,
padding: "0",
border: "0",
borderRadius: "6px",
minHeight: "auto",
}),
dropdownIndicator: (defaultStyles) => ({
...defaultStyles,
display: "none",
}),
valueContainer: (defaultStyles) => ({
...defaultStyles,
padding: "0",
}),
placeholder: (defaultStyles) => ({
...defaultStyles,
margin: "0",
}),
}}
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
{options.map((data) => (
<DropdownMenuItem key={data.slug} className="focus:outline-none">
{data.slug === "add-new" ? (
<DropdownItem StartIcon={FiPlus} color="minimal" href="/apps/categories/calendar">
{t("install_new_calendar_app")}
</DropdownItem>
) : (
<InstallAppButton
type={data.type}
render={(installProps) => {
const props = { ...installProps } as any;
return (
<DropdownItem {...props} color="minimal" type="button">
<span className="flex items-center gap-x-2">
{data.image && <img className="h-5 w-5" src={data.image} alt={data.label} />}
{`${t("add")} ${data.label}`}
</span>
</DropdownItem>
);
}}
/>
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</Dropdown>
);
}}
/>
@@ -1,139 +0,0 @@
import classNames from "classnames";
import React, { useEffect, useState } from "react";
import Select from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { DestinationCalendar } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
interface Props {
onChange: (value: { externalId: string; integration: string }) => void;
isLoading?: boolean;
hidePlaceholder?: boolean;
/** The external Id of the connected calendar */
value: string | undefined;
destinationCalendar?: DestinationCalendar | null;
}
const DestinationCalendarSelector = ({
onChange,
isLoading,
value,
hidePlaceholder,
destinationCalendar,
}: Props): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.connectedCalendars.useQuery();
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
// Extra styles to show prefixed text in react-select
const content = (hidePlaceholder = false) => {
if (!hidePlaceholder) {
return {
alignItems: "center",
display: "flex",
":before": {
content: `'${t("select_destination_calendar")}:'`,
display: "block",
marginRight: 8,
},
};
}
return {};
};
useEffect(() => {
const selected = query.data?.connectedCalendars
.map((connected) => connected.calendars ?? [])
.flat()
.find((cal) => cal.externalId === value);
if (selected) {
setSelectedOption({
value: `${selected.integration}:${selected.externalId}`,
label: `${selected.name} (${selected.externalId})` || "",
});
}
}, [query.data?.connectedCalendars, value]);
if (!query.data?.connectedCalendars.length) {
return null;
}
const options =
query.data.connectedCalendars.map((selectedCalendar) => ({
key: selectedCalendar.credentialId,
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
options: (selectedCalendar.calendars ?? [])
.filter((cal) => cal.readOnly === false)
.map((cal) => ({
label: `${cal.name} (${cal.externalId})` || "",
value: `${cal.integration}:${cal.externalId}`,
})),
})) ?? [];
const defaultCalendarSelectedString = destinationCalendar?.externalId
? `(${
destinationCalendar.externalId.length > 15
? destinationCalendar.externalId.substring(0, 15) + "..."
: destinationCalendar.externalId
})`
: "";
return (
<div
className="relative flex items-center"
title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
<Select
name="primarySelectedCalendar"
placeholder={
!hidePlaceholder ? (
`${t("select_destination_calendar")}`
) : (
<span className="whitespace-nowrap">
{t("default_calendar_selected")} {defaultCalendarSelectedString}
</span>
)
}
options={options}
styles={{
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "6px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
width: "100%",
},
};
},
}}
isSearchable={false}
className={classNames(
"block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
!hidePlaceholder && "font-medium"
)}
onChange={(option) => {
setSelectedOption(option);
if (!option) {
return;
}
/* Split only the first `:`, since Apple uses the full URL as externalId */
const [integration, externalId] = option.value.split(/:(.+)/);
onChange({
integration,
externalId,
});
}}
isLoading={isLoading}
value={selectedOption}
components={{
IndicatorSeparator: () => null,
}}
/>
</div>
);
};
export default DestinationCalendarSelector;
@@ -8,54 +8,61 @@ import { FiLink } from "@calcom/ui/components/icon";
import type { Props } from "./pages/AvailabilityPage";
const excludeNullValues = (value: unknown) => !!value;
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
const { t } = useLocale();
return locations.length ? (
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
{locations.map((location, index) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
// It's possible that the location app got uninstalled
return null;
}
const renderLocations = locations.map((location, index) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
// It's possible that the location app got uninstalled
return null;
}
if (eventLocationType.variable === "hostDefault") {
return null;
}
const translateAbleKeys = [
"attendee_in_person",
"in_person",
"attendee_phone_number",
"link_meeting",
"organizer_phone_number",
];
const translateAbleKeys = [
"attendee_in_person",
"in_person",
"attendee_phone_number",
"link_meeting",
"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:")
? eventLocationType.label
: translateAbleKeys.includes(locationKey)
? t(locationKey)
: locationKey;
return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? (
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
) : (
<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`}
/>
return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? (
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
) : (
<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]" : ""
)}
<Tooltip content={translatedLocation}>
<p className="line-clamp-1">{translatedLocation}</p>
</Tooltip>
</div>
);
})}
alt={`${eventLocationType.label} icon`}
/>
)}
<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>
) : null;
}
@@ -33,6 +33,7 @@ interface ISetLocationDialog {
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
selection?: LocationOption;
booking?: BookingItem;
isTeamEvent?: boolean;
defaultValues?: LocationObject[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean;
@@ -74,6 +75,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
saveLocation,
selection,
booking,
isTeamEvent,
setShowLocationModal,
isOpenDialog,
defaultValues,
@@ -290,7 +292,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
query={locationsQuery}
success={({ data }) => {
if (!data.length) return null;
const locationOptions = [...data];
const locationOptions = [...data].filter((option) => {
return !isTeamEvent ? option.label !== "Conferencing" : true;
});
if (booking) {
locationOptions.map((location) =>
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
@@ -10,11 +10,12 @@ import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } from "@calcom/core/event";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
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 type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -183,9 +184,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
setRedirectUrlVisible(e);
onChange(e ? value : "");
}}>
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
<div className="lg:-ml-2">
{/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
<div className="lg:-ml-2 lg:-mb-2">
<TextField
className="w-full"
label={t("redirect_success_booking")}
labelSrOnly
placeholder={t("external_redirect_url")}
@@ -194,21 +196,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")}
/>
<div className="mt-2 flex">
<Checkbox
description={t("disable_success_page")}
// Disable if it's not Self Hosted or if the redirect url is not set
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")}
{...formMethods.register("metadata.disableSuccessPage")}
/>
{/*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
className={classNames(
"p-1 text-sm text-orange-600",
formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
)}>
{t("redirect_url_warning")}
</div>
</div>
</SettingsToggle>
@@ -426,6 +426,7 @@ const IntervalLimitItem = ({
required
type="number"
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
className="mb-0"
placeholder={`${value}`}
min={step}
step={step}
+44 -25
View File
@@ -4,7 +4,7 @@ import { isValidPhoneNumber } from "libphonenumber-js";
import { Trans } from "next-i18next";
import Link from "next/link";
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 type { MultiValue } from "react-select";
import { z } from "zod";
@@ -16,7 +16,17 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import { slugify } from "@calcom/lib/slugify";
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 { EditLocationDialog } from "@components/dialog/EditLocationDialog";
@@ -35,14 +45,31 @@ const getLocationFromType = (
}
};
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => {
for (const locationType of options) {
for (const location of locationType.options) {
if (location.value === type && location.disabled === false) {
return location;
}
}
}
interface DescriptionEditorProps {
description?: string | null;
}
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 = (
@@ -53,12 +80,16 @@ export const EventSetupTab = (
) => {
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const { eventType, locationOptions, team, destinationCalendar } = props;
const { eventType, team, destinationCalendar } = props;
const [showLocationModal, setShowLocationModal] = useState(false);
const [editingLocationType, setEditingLocationType] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
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) => ({
value: mins,
label: t("multiple_duration_mins", { count: mins }),
@@ -198,14 +229,6 @@ export const EventSetupTab = (
return (
<li
onClick={() => {
locationFormMethods.setValue("locationType", location.type);
locationFormMethods.unregister("locationLink");
locationFormMethods.unregister("locationAddress");
locationFormMethods.unregister("locationPhoneNumber");
setEditingLocationType(location.type);
openLocationModal(location.type);
}}
key={`${location.type}${index}`}
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer">
<div className="flex items-center justify-between">
@@ -287,12 +310,7 @@ export const EventSetupTab = (
/>
<div>
<Label>{t("description")}</Label>
<Editor
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
/>
<DescriptionEditor description={eventType?.description} />
</div>
<TextField
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 */}
<EditLocationDialog
isTeamEvent={!!team}
isOpenDialog={showLocationModal}
setShowLocationModal={setShowLocationModal}
saveLocation={saveLocation}
@@ -1,11 +1,11 @@
export type EventTypeDescriptionSafeProps = {
eventType: { description: string | null };
eventType: { description: string | null; descriptionAsSafeHTML: string | null };
};
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
// @ts-expect-error: @see packages/prisma/middleware/eventTypeDescriptionParseAndSanitize.ts
if (eventType.description) props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML };
if (eventType.description)
props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML || "" };
return <div {...props} />;
};
@@ -1,9 +1,8 @@
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterInputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
interface ICreateEventsOnCalendarSelectProps {
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
}
+6 -5
View File
@@ -2,13 +2,14 @@ import Link from "next/link";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { Avatar } from "@calcom/ui";
type TeamType = NonNullable<TeamWithMembers>;
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 { t } = useLocale();
@@ -30,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
<>
<div
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) {
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 (
<div>
<Members members={team.members} teamName={team.name} />
+5 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.7.5",
"version": "2.7.8",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -11,6 +11,7 @@
"dx": "yarn dev",
"test-codegen": "yarn playwright codegen http://localhost:3000",
"type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit",
"build": "next build",
"start": "next start",
"lint": "eslint . --ignore-path .gitignore",
@@ -64,7 +65,7 @@
"@types/turndown": "^5.0.1",
"@vercel/edge-config": "^0.1.1",
"@vercel/edge-functions-ui": "^0.2.1",
"@vercel/og": "^0.0.21",
"@vercel/og": "^0.5.0",
"accept-language-parser": "^1.5.0",
"async": "^3.2.4",
"bcryptjs": "^2.4.3",
@@ -119,6 +120,7 @@
"react-window": "^1.8.7",
"remark": "^14.0.2",
"rrule": "^2.7.1",
"sanitize-html": "^2.10.0",
"schema-dts": "^1.1.0",
"short-uuid": "^4.2.0",
"strip-markdown": "^5.0.0",
@@ -156,6 +158,7 @@
"@types/react-phone-number-input": "^3.0.14",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@types/sanitize-html": "^2.9.0",
"@types/stripe": "^8.0.417",
"@types/uuid": "8.3.1",
"autoprefixer": "^10.4.12",
+42 -5
View File
@@ -3,10 +3,10 @@ import Link from "next/link";
import { useRouter } from "next/router";
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 { 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";
@@ -40,6 +40,43 @@ export default function Custom404() {
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
const isSignup = router.asPath.startsWith("/signup");
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 (
<>
@@ -62,7 +99,7 @@ export default function Custom404() {
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900">
{t("signup_requires")}
</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 className="mt-12">
<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">
<li className="px-4 py-2">
<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">
<div className="flex-shrink-0">
<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>
<div className="mt-8">
<Link
href={`${WEBSITE_URL}/enterprise`}
href="https://cal.com/sales"
className="text-base font-medium text-black hover:text-gray-500">
{t("contact_sales")}
<span aria-hidden="true"> &rarr;</span>
+6 -2
View File
@@ -24,7 +24,7 @@ import defaultEvents, {
} from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
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 prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
@@ -147,7 +147,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<>
<div
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) => ({
...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}));
const isSingleUser = users.length === 1;
@@ -352,9 +353,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
})
: [];
const safeBio = markdownToSafeHTML(user.bio) || "";
return {
props: {
users,
safeBio,
profile,
user: {
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 hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
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 { isBrandingHidden } from "@lib/isBrandingHidden";
@@ -59,7 +59,6 @@ Type.isThemeSupported = true;
const paramsSchema = z.object({ type: z.string(), user: z.string() });
async function getUserPageProps(context: GetStaticPropsContext) {
// 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 { privacyFilteredLocations } = await import("@calcom/app-store/locations");
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 };
const [eventType]: ((typeof user.eventTypes)[number] & {
@@ -153,7 +149,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
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
const hasActiveTeam =
+4 -2
View File
@@ -13,7 +13,10 @@ import {
getGroupName,
getUsernameList,
} 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 { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
import {
customInputSchema,
@@ -21,8 +24,6 @@ import {
userMetadata as userMetadataSchema,
} from "@calcom/prisma/zod-utils";
import type { GetBookingType } from "@lib/getBooking";
import getBooking from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage";
@@ -189,6 +190,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
slug: u.username,
theme: u.theme,
})),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
};
})[0];
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
@@ -48,8 +49,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
eventType: {
select: {
recurringEvent: true,
bookingFields: true,
},
},
responses: true,
uid: true,
destinationCalendar: true,
},
@@ -96,6 +99,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
title: booking.title,
description: booking.description || undefined,
customInputs: isPrismaObjOrUndefined(booking.customInputs),
...getCalEventResponses({
bookingFields: booking.eventType?.bookingFields ?? null,
booking,
}),
location: booking.location ?? "",
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
+37 -4
View File
@@ -1,4 +1,12 @@
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) {
const apiKey = req.headers.authorization || req.query.apiKey;
@@ -11,10 +19,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return;
}
/**
* TODO:
* Remove this endpoint
*/
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
let { page: pageNumber } = querySchema.parse(req.query);
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 });
}
+3 -3
View File
@@ -88,7 +88,7 @@ export default async function handler(req: NextApiRequest) {
ogConfig
) as { body: Buffer };
return new Response(img.body, { status: 200 });
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
}
case "app": {
const { name, description, slug } = appSchema.parse({
@@ -101,7 +101,7 @@ export default async function handler(req: NextApiRequest) {
body: Buffer;
};
return new Response(img.body, { status: 200 });
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
}
case "generic": {
@@ -115,7 +115,7 @@ export default async function handler(req: NextApiRequest) {
body: Buffer;
};
return new Response(img.body, { status: 200 });
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
}
default:
+3 -3
View File
@@ -14,7 +14,7 @@ import { FiSearch } from "@calcom/ui/components/icon";
import AppsLayout from "@components/apps/layouts/AppsLayout";
import { ssgInit } from "@server/lib/ssg";
import { ssrInit } from "@server/lib/ssr";
const tabs: HorizontalTabItemProps[] = [
{
@@ -88,7 +88,7 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
const ssg = await ssgInit(context);
const ssr = await ssrInit(context);
const session = await getServerSession({ req, res });
@@ -119,7 +119,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return b.count - a.count;
}),
appStore,
trpcState: ssg.dehydrate(),
trpcState: ssr.dehydrate(),
},
};
};
+14 -135
View File
@@ -6,7 +6,7 @@ import type { GetServerSidePropsContext } from "next";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { RRule } from "rrule";
import { z } from "zod";
@@ -36,6 +36,7 @@ import {
formatToLocalizedTimezone,
} from "@calcom/lib/date-fns";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { getBookingWithResponses } from "@calcom/lib/getBooking";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
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 { timeZone } from "@lib/clock";
import { getBookingWithResponses } from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import CancelBooking from "@components/booking/CancelBooking";
@@ -59,114 +59,6 @@ import EventReservationSchema from "@components/schemas/EventReservationSchema";
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>;
const stringToBoolean = z
@@ -176,13 +68,14 @@ const stringToBoolean = z
const querySchema = z.object({
uid: z.string(),
allRemainingBookings: stringToBoolean,
email: z.string().optional(),
eventTypeSlug: z.string().optional(),
cancel: stringToBoolean,
allRemainingBookings: stringToBoolean,
changes: stringToBoolean,
reschedule: stringToBoolean,
isSuccessBookingPage: stringToBoolean,
formerTime: z.string().optional(),
email: z.string().optional(),
seatReferenceUid: z.string().optional(),
});
@@ -404,9 +297,6 @@ export default function Success(props: SuccessProps) {
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
{isSuccessBookingPage && !isCancellationMode && eventType.successRedirectUrl ? (
<RedirectionToast url={eventType.successRedirectUrl} />
) : null}{" "}
<div
className={classNames(
shouldAlignCentrally ? "text-center" : "",
@@ -569,7 +459,8 @@ export default function Success(props: SuccessProps) {
</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]) => {
const field = eventType.bookingFields.find((field) => field.name === name);
// We show location in the "where" section
@@ -584,15 +475,10 @@ export default function Success(props: SuccessProps) {
return (
<>
<div className="mt-9 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}>
{response.toString()}
</p>
</div>
<div className="mt-4 font-medium">{label}</div>
<p className="break-words" data-testid="field-response" data-fob-field={field.name}>
{response.toString()}
</p>
</>
);
})}
@@ -985,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 = (
eventType: {
seatsPerTimeSlot?: number | null;
@@ -1021,9 +900,9 @@ const handleSeatsEventTypeOnBooking = (
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const parsedQuery = schema.safeParse(context.query);
const parsedQuery = querySchema.safeParse(context.query);
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({
where: {
@@ -1120,7 +999,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
};
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl && !cancel) {
if (eventType.successRedirectUrl && isSuccessBookingPage && !cancel) {
return {
redirect: {
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 { parseRecurringEvent } from "@calcom/lib";
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 prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { GetBookingType } from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
@@ -119,6 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
hideBranding: u.hideBranding,
timeZone: u.timeZone,
})),
descriptionAsSafeHTML: markdownToSafeHTML(hashedLink.eventType.description),
});
const [user] = users;
+2
View File
@@ -1,6 +1,7 @@
import type { GetServerSidePropsContext } from "next";
import { parseRecurringEvent } from "@calcom/lib";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma from "@calcom/prisma";
import { bookEventTypeSelect } from "@calcom/prisma/selects";
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -93,6 +94,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
brandColor: u.brandColor,
darkBrandColor: u.darkBrandColor,
})),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
};
})[0];
+11 -25
View File
@@ -13,11 +13,9 @@ import type { EventLocationType } from "@calcom/core/location";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { validateIntervalLimitOrder } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
import getEventTypeById from "@calcom/lib/getEventTypeById";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -65,6 +63,7 @@ export type FormValues = {
hostPhoneNumber?: string;
displayLocationPublicly?: boolean;
phone?: string;
hostDefault?: string;
}[];
customInputs: CustomInputParsed[];
schedule: number | null;
@@ -365,9 +364,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
};
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
const { data, isLoading } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
if (isLoading || !data) return null;
return <EventTypePage {...data} />;
const { data } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
return <EventTypePage {...(data as EventTypeSetupProps)} />;
};
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
@@ -393,26 +392,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
};
}
try {
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma });
return {
props: {
// 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;
}
await ssr.viewer.eventTypes.get.prefetch({ id: typeParam });
return {
props: {
type: typeParam,
trpcState: ssr.dehydrate(),
},
};
};
export default EventTypePageWrapper;
+1 -1
View File
@@ -141,7 +141,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
</div>
<EventTypeDescription
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
eventType={type}
eventType={{ ...type, descriptionAsSafeHTML: type.safeDescription }}
shortenDescription
/>
</Link>
+27 -15
View File
@@ -1,3 +1,4 @@
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import {
AverageEventDurationChart,
BookingKPICards,
@@ -7,7 +8,6 @@ import {
PopularEventsTable,
} from "@calcom/features/insights/components";
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
import { useFilterContext } from "@calcom/features/insights/context/provider";
import { Filters } from "@calcom/features/insights/filters";
import Shell from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips";
@@ -19,15 +19,11 @@ import { FiRefreshCcw, FiUserPlus, FiUsers } from "@calcom/ui/components/icon";
const Heading = () => {
const { t } = useLocale();
const {
filter: { selectedTeamName },
} = useFilterContext();
return (
<div className="min-w-52">
<h3 className="font-cal max-w-28 sm:max-w-72 md:max-w-80 hidden truncate text-xl font-semibold tracking-wide text-black md:block xl:max-w-full">
{t("analytics_for_organisation", {
organisationName: selectedTeamName,
})}
<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>
@@ -39,17 +35,17 @@ export default function InsightsPage() {
const { data: user } = trpc.viewer.me.useQuery();
const features = [
{
icon: <FiUsers className="h-5 w-5 text-red-500" />,
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 text-blue-500" />,
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 text-green-500" />,
icon: <FiUserPlus className="h-5 w-5" />,
title: t("spot_popular_event_types"),
description: t("spot_popular_event_types_description"),
},
@@ -62,7 +58,7 @@ export default function InsightsPage() {
title={t("make_informed_decisions")}
description={t("make_informed_decisions_description")}
features={features}
background="/banners/insights.jpg"
background="/tips/insights.jpg"
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
@@ -79,7 +75,7 @@ export default function InsightsPage() {
<></>
) : (
<FiltersProvider>
<div className="mb-4 mt-0 ml-auto flex w-full flex-wrap justify-between md:-mt-8">
<div className="mb-8 mt-0 ml-auto flex w-full flex-wrap justify-between md:-mt-8">
<Heading />
<Filters />
</div>
@@ -98,7 +94,7 @@ export default function InsightsPage() {
<LeastBookedTeamMembersTable />
</div>
<small className="block text-center text-gray-600">
{t("looking_for_more_analytics")}
{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...">
@@ -114,3 +110,19 @@ export default function InsightsPage() {
</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: {},
};
};
@@ -3,7 +3,7 @@ import { useState } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
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 EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
@@ -32,11 +32,14 @@ const TwoFactorAuthView = () => {
if (isLoading) return <SkeletonLoader />;
const isCalProvider = user?.identityProvider === "CAL";
return (
<>
<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">
<Switch
disabled={!isCalProvider}
checked={user?.twoFactorEnabled}
onCheckedChange={() =>
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
@@ -19,7 +19,7 @@ const OnboardTeamMembersPage = () => {
};
OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => (
<WizardLayout currentStep={1} maxSteps={2}>
<WizardLayout currentStep={2} maxSteps={2}>
{page}
</WizardLayout>
);
+9 -2
View File
@@ -3,7 +3,7 @@ import Head from "next/head";
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getLayout } from "@components/layouts/WizardLayout";
import WizardLayout from "@components/layouts/WizardLayout";
const CreateNewTeamPage = () => {
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;
+10 -3
View File
@@ -10,7 +10,7 @@ import { CAL_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
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 { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
@@ -113,7 +113,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
<>
<div
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,
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 {
props: {
team,
team: { ...team, safeBio, members },
trpcState: ssr.dehydrate(),
},
} as const;
+4 -3
View File
@@ -4,12 +4,13 @@ import type { LocationObject } from "@calcom/core/location";
import { privacyFilteredLocations } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib";
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 { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
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 { EmbedProps } from "@lib/withEmbedSsr";
@@ -63,7 +64,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
select: {
id: true,
name: true,
avatar: true,
username: true,
timeZone: true,
hideBranding: true,
@@ -170,6 +170,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
hideBranding,
timeZone,
})),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
});
eventTypeObject.availability = [];
+4 -2
View File
@@ -5,12 +5,13 @@ import type { LocationObject } from "@calcom/app-store/locations";
import { privacyFilteredLocations } from "@calcom/app-store/locations";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
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 { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import type { GetBookingType } from "@lib/getBooking";
import getBooking from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage";
@@ -121,6 +122,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
image: u.avatar,
slug: u.username,
})),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
};
})[0];
+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 ({
+1 -1
View File
@@ -45,7 +45,7 @@ export const createEmbedsFixture = (page: Page) => {
}
api("on", {
action: "*",
callback: (e: any) => {
callback: (e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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
+10 -4
View File
@@ -97,13 +97,19 @@ test.describe("Reschedule Tests", async () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking.uid } });
const rescheduledBooking = await prisma.booking.findFirst({ where: { uid: booking.uid } });
const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: booking.uid } });
const rescheduledBooking = await prisma.booking.findFirstOrThrow({ where: { uid: booking.uid } });
expect(newBooking).not.toBeNull();
expect(rescheduledBooking).toBeNull();
expect(rescheduledBooking.status).toBe(BookingStatus.CANCELLED);
await prisma.booking.delete({ where: { id: newBooking?.id } });
await prisma.booking.deleteMany({
where: {
id: {
in: [newBooking.id, rescheduledBooking.id],
},
},
});
});
test("Unpaid rescheduling should go to payment page", async ({ page, users, bookings, payments }) => {
+3 -1
View File
@@ -4,4 +4,6 @@ Disallow: /api
Disallow: /static/locales
# 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/
+14 -6
View File
@@ -404,6 +404,7 @@
"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.",
"2fa": "Two-Factor Authentication",
"2fa_disabled": "Two-Factor authentication can only be enabled for email and password authentication",
"enable_2fa": "Enable 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.",
@@ -1218,6 +1219,7 @@
"impersonation": "Impersonation",
"impersonation_description": "Settings to manage user impersonation",
"users": "Users",
"user": "User",
"profile_description": "Manage settings for your {{appName}} profile",
"users_description": "Here you can find a list of all users",
"users_listing": "User listing",
@@ -1499,7 +1501,7 @@
"date_overrides_update_btn": "Update Override",
"event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "Set as default",
"hide_eventtype_details": "Hide EventType Details",
"hide_eventtype_details": "Hide event type details",
"show_navigation": "Show navigation",
"hide_navigation": "Hide navigation",
"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_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)",
"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",
"username_already_taken": "Username is already taken",
"assignment": "Assignment",
@@ -1608,6 +1610,7 @@
"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.",
"change_default_conferencing_app": "Set as default",
"organizer_default_conferencing_app": "Organizer's default app",
"under_maintenance": "Down for maintenance",
"under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.",
"event_type_seats": "{{numberOfSeats}} seats",
@@ -1670,7 +1673,7 @@
"default_conferencing_bulk_title": "Bulk update existing event types",
"requires_at_least_one_schedule": "You are required to have at least one schedule",
"default_conferencing_bulk_description": "Update the locations for the selected event types",
"looking_for_more_analytics": "Looking for more analytics?",
"looking_for_more_insights": "Looking for more Insights?",
"add_filter": "Add filter",
"select_user": "Select User",
"select_event_type": "Select Event Type",
@@ -1686,7 +1689,12 @@
"events_rescheduled": "Events Rescheduled",
"from_last_period": "from last period",
"from_to_date_period": "From: {{startDate}} To: {{endDate}}",
"analytics_for_organisation": "Analytics for {{organisationName}}",
"subtitle_analytics": "This is a organisation analytics",
"event_trends": "Event Trends"
"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_description": "Configuración para administrar la suplantación del usuario",
"users": "Usuarios",
"user": "Usuario",
"profile_description": "Administra los ajustes para tu perfil de {{appName}}",
"users_description": "Aquí puede encontrar una lista de todos los usuarios",
"users_listing": "Lista de usuarios",

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

+2 -2
View File
@@ -148,8 +148,6 @@ select:focus {
--tw-ring-color: var(--brand-color);
border-color: var(--brand-color);
}
/* https://stackoverflow.com/a/69308190 */
input[type='text']:focus { box-shadow: none; }
@layer components {
@@ -497,3 +495,5 @@ hr {
::-webkit-search-cancel-button {
-webkit-appearance: none;
}
+3 -1
View File
@@ -70,6 +70,7 @@
"test-playwright": "yarn playwright test --config=playwright.config.ts",
"test": "jest",
"type-check": "turbo run type-check",
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
"web": "yarn workspace @calcom/web"
},
"devDependencies": {
@@ -86,8 +87,9 @@
"jest-watch-select-projects": "^2.0.0",
"jest-watch-typeahead": "^2.0.0",
"lint-staged": "^12.5.0",
"prettier": "^2.8.4",
"prettier": "^2.8.6",
"ts-jest": "^28.0.8",
"tsc-absolute": "^1.0.0",
"typescript": "^4.9.4"
},
"dependencies": {
+3 -1
View File
@@ -1,7 +1,7 @@
import fs from "fs";
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 execSync from "./utils/execSync";
@@ -127,6 +127,8 @@ export const BaseAppFork = {
.replace(/_DESCRIPTION_/g, description)
.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 }) {
@@ -40,6 +40,7 @@ export const EventTypeAddonMap = {
fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")),
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
"google-tag-manager": dynamic(() => import("./google-tag-manager/components/EventTypeAppCardInterface")),
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
qr_code: dynamic(() => import("./qr_code/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 ga4_zod_ts } from "./ga4/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 hubspot_zod_ts } from "./hubspot/zod";
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
@@ -30,6 +31,7 @@ export const appKeysSchemas = {
fathom: fathom_zod_ts,
ga4: ga4_zod_ts,
giphy: giphy_zod_ts,
"google-tag-manager": google_tag_manager_zod_ts,
googlecalendar: googlecalendar_zod_ts,
hubspot: hubspot_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 ga4_config_json from "./ga4/config.json";
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 googlevideo__metadata_ts } from "./googlevideo/_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 sirius_video_config_json from "./sirius_video/config.json";
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 telegram_config_json from "./telegram/config.json";
import basic_config_json from "./templates/basic/config.json";
@@ -75,6 +77,7 @@ export const appStoreMetadata = {
fathom: fathom_config_json,
ga4: ga4_config_json,
giphy: giphy__metadata_ts,
"google-tag-manager": google_tag_manager_config_json,
googlecalendar: googlecalendar__metadata_ts,
googlevideo: googlevideo__metadata_ts,
hubspot: hubspot__metadata_ts,
@@ -97,6 +100,7 @@ export const appStoreMetadata = {
signal: signal_config_json,
sirius_video: sirius_video_config_json,
stripepayment: stripepayment__metadata_ts,
sylapsvideo: sylapsvideo_config_json,
tandemvideo: tandemvideo__metadata_ts,
telegram: telegram_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 ga4_zod_ts } from "./ga4/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 hubspot_zod_ts } from "./hubspot/zod";
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
@@ -30,6 +31,7 @@ export const appDataSchemas = {
fathom: fathom_zod_ts,
ga4: ga4_zod_ts,
giphy: giphy_zod_ts,
"google-tag-manager": google_tag_manager_zod_ts,
googlecalendar: googlecalendar_zod_ts,
hubspot: hubspot_zod_ts,
larkcalendar: larkcalendar_zod_ts,
@@ -17,6 +17,7 @@ export const apiHandlers = {
fathom: import("./fathom/api"),
ga4: import("./ga4/api"),
giphy: import("./giphy/api"),
"google-tag-manager": import("./google-tag-manager/api"),
googlecalendar: import("./googlecalendar/api"),
googlevideo: import("./googlevideo/api"),
hubspot: import("./hubspot/api"),
@@ -39,6 +40,7 @@ export const apiHandlers = {
signal: import("./signal/api"),
sirius_video: import("./sirius_video/api"),
stripepayment: import("./stripepayment/api"),
sylapsvideo: import("./sylapsvideo/api"),
tandemvideo: import("./tandemvideo/api"),
telegram: import("./telegram/api"),
basic: import("./templates/basic/api"),
+1
View File
@@ -10,6 +10,7 @@ items:
{DESCRIPTION}
### Cron's most valuable features
<!-- From "FAQs: What are Cron’s most valuable features?"
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({});
@@ -165,6 +165,7 @@ export default class GoogleCalendarService implements Calendar {
type: "google_calendar",
password: "",
url: "",
iCalUID: event.data.iCalUID,
});
}
);
@@ -201,6 +202,9 @@ export default class GoogleCalendarService implements Calendar {
id: String(event.organizer.id),
organizer: true,
responseStatus: "accepted",
email: event.destinationCalendar?.externalId
? event.destinationCalendar.externalId
: event.organizer.email,
},
// eslint-disable-next-line
...eventAttendees,
@@ -268,6 +272,7 @@ export default class GoogleCalendarService implements Calendar {
type: "google_calendar",
password: "",
url: "",
iCalUID: evt.data.iCalUID,
});
}
return resolve(evt?.data);
+2
View File
@@ -20,6 +20,7 @@ import * as plausible from "./plausible";
import * as salesforce from "./salesforce";
import * as sendgrid from "./sendgrid";
import * as stripepayment from "./stripepayment";
import * as sylapsvideo from "./sylapsvideo";
import * as tandemvideo from "./tandemvideo";
import * as vital from "./vital";
import * as wipemycalother from "./wipemycalother";
@@ -38,6 +39,7 @@ const appStore = {
hubspot,
huddle01video,
jitsivideo,
sylapsvideo,
larkcalendar,
office365calendar,
office365video,
+25 -4
View File
@@ -12,13 +12,20 @@ export type DefaultEventLocationType = {
type: DefaultEventLocationTypeEnum;
label: string;
messageForOrganizer: string;
category: "in person" | "other" | "phone";
category: "in person" | "conferencing" | "other" | "phone";
iconUrl: string;
urlRegExp?: string;
// 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";
defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "phone";
variable:
| "locationType"
| "locationAddress"
| "address"
| "locationLink"
| "locationPhoneNumber"
| "phone"
| "hostDefault";
defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone";
} & (
| {
organizerInputType: "phone" | "text" | null;
@@ -60,6 +67,7 @@ export enum DefaultEventLocationTypeEnum {
*/
UserPhone = "userPhone",
Link = "link",
Conferencing = "conferencing",
}
export const defaultLocations: DefaultEventLocationType[] = [
@@ -88,6 +96,17 @@ export const defaultLocations: DefaultEventLocationType[] = [
iconUrl: "/map-pin.svg",
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,
type: DefaultEventLocationTypeEnum.Link,
@@ -130,7 +149,9 @@ export const defaultLocations: DefaultEventLocationType[] = [
export type LocationObject = {
type: string;
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
export type BookingLocationValue = string;
@@ -73,7 +73,9 @@ export default class Office365CalendarService implements Calendar {
body: JSON.stringify(this.translateEvent(event)),
});
return handleErrorsJson(response);
const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
return { ...responseJson, iCalUID: responseJson.iCalUId };
} catch (error) {
this.log.error(error);
@@ -88,7 +90,9 @@ export default class Office365CalendarService implements Calendar {
body: JSON.stringify(this.translateEvent(event)),
});
return handleErrorsRaw(response);
const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
return { ...responseJson, iCalUID: responseJson.iCalUId };
} catch (error) {
this.log.error(error);
+1
View File
@@ -8,6 +8,7 @@
],
"scripts": {
"type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit",
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json"
@@ -1,9 +1,12 @@
import type { ChangeEvent } from "react";
import type { Settings, Widgets, SelectWidgetProps } from "react-awesome-query-builder";
// Figure out why routing-forms/env.d.ts doesn't work
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
import BasicConfig from "react-awesome-query-builder/lib/config/basic";
import { EmailField } from "@calcom/ui";
import widgetsComponents from "../widgets";
const {
@@ -92,8 +95,18 @@ const widgets: Widgets & { [key in keyof Widgets]: Widgets[key] & { type: string
if (!props) {
return <div />;
}
// TODO: Use EmailField component for Routing Form Email field
return <TextWidget type="email" {...props} />;
return (
<EmailField
onChange={(e: ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
props.setValue(val);
}}
containerClassName="w-full"
className="dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500"
{...props}
/>
);
},
},
};
@@ -102,7 +102,7 @@ export default function RoutingForms({
title={t("teams_plan_required")}
description={t("routing_forms_are_a_great_way")}
features={features}
background="/banners/routing-forms.jpg"
background="/tips/routing-forms.jpg"
isParentLoading={isLoading && <SkeletonLoaderTeamList />}
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
@@ -0,0 +1,6 @@
---
items:
- /api/app-store/sylaps/sylaps1.png
---
Free Audio and Video Conferencing, Online Collaboration, Screen Sharing on web browser, mobile and desktop.
+45
View File
@@ -0,0 +1,45 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
/**
* This is an example endpoint for an app, these will run under `/api/integrations/[...args]`
* @param req
* @param res
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
const appType = "sylaps_video";
try {
const alreadyInstalled = await prisma.credential.findFirst({
where: {
type: appType,
userId: req.session.user.id,
},
});
if (alreadyInstalled) {
throw new Error("Already installed");
}
const installation = await prisma.credential.create({
data: {
type: appType,
key: {},
userId: req.session.user.id,
appId: "sylaps",
},
});
if (!installation) {
throw new Error("Unable to create user credential for sylaps");
}
} catch (error: unknown) {
if (error instanceof Error) {
return res.status(500).json({ message: error.message });
}
return res.status(500);
}
return res.status(200).json({ url: getInstalledAppPath({ variant: "conferencing", slug: "sylaps" }) });
}
@@ -0,0 +1 @@
export { default as add } from "./add";
@@ -0,0 +1 @@
.gitkeep
@@ -0,0 +1,23 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Sylaps",
"title": "Sylaps",
"slug": "sylaps",
"type": "sylaps_video",
"imageSrc": "/api/app-store/sylaps/icon.svg",
"logo": "/api/app-store/sylaps/icon.svg",
"url": "https://cal.com/apps/sylaps",
"variant": "conferencing",
"categories": ["video"],
"publisher": "Sylaps Inc",
"email": "support@sylaps.com",
"description": "Free Audio and Video Conferencing, Online Collaboration, Screen Sharing on web browser, mobile and desktop.",
"__createdUsingCli": true,
"appData": {
"location": {
"linkType": "dynamic",
"type": "integrations:sylaps_video",
"label": "Sylaps"
}
}
}
+2
View File
@@ -0,0 +1,2 @@
export * as lib from "./lib";
export * as api from "./api";
@@ -0,0 +1,34 @@
import { v4 as uuidv4 } from "uuid";
import type { PartialReference } from "@calcom/types/EventManager";
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
const SylapsApiAdapter = (): VideoApiAdapter => {
return {
getAvailability: () => {
return Promise.resolve([]);
},
createMeeting: async (): Promise<VideoCallData> => {
const meetingID = uuidv4();
return Promise.resolve({
type: "sylaps_video",
id: meetingID,
password: "",
url: "https://sylaps.com/r/" + meetingID,
});
},
deleteMeeting: async (): Promise<void> => {
Promise.resolve();
},
updateMeeting: (bookingRef: PartialReference): Promise<VideoCallData> => {
return Promise.resolve({
type: "sylaps_video",
id: bookingRef.meetingId as string,
password: bookingRef.meetingPassword as string,
url: bookingRef.meetingUrl as string,
});
},
};
};
export default SylapsApiAdapter;
@@ -0,0 +1 @@
export { default as VideoApiAdapter } from "./VideoApiAdapter";
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/sylapsvideo",
"version": "0.0.0",
"main": "./index.ts",
"description": "Free Audio and Video Conferencing, Online Collaboration, Screen Sharing on web browser, mobile and desktop.",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
}
}
@@ -0,0 +1 @@
<svg viewBox="0 0 318 318" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><use xlink:href="#a" x="255" width="318" height="318" transform="matrix(1.08202 0 0 1.08518 -289.504 -13.007)"/><path d="M126.988 77.707c6.621-7.672 13.161-15.177 19.543-22.814.431-.516.036-2.425-.599-3.021-6.041-5.662-11.83-11.695-18.427-16.64C109.3 21.586 89.027 17.757 67.314 25.074 36.697 35.392 18.186 66.19 22.801 98.267c4.15 28.837 24.33 51.186 52.908 56.744 28.895 5.62 54.399-2.534 74.886-23.504 11.478-11.749 21.051-25.341 31.726-37.896 7.402-8.705 14.589-17.75 23.007-25.393 10.772-9.779 23.805-13.01 37.68-6.757 14.16 6.382 20.803 21.031 17.142 36.146-3.611 14.908-16.076 24.446-31.371 23.875-7.912-.296-14.583-3.867-20.539-8.703-5.03-4.084-9.701-8.611-14.855-13.233l-19.731 24.625c9.626 10.85 19.982 20.108 32.778 26.305 12.635 6.119 25.924 7.785 39.613 4.901 34.945-7.361 57.368-39.123 52.927-74.668-3.626-29.021-24.928-52.216-53.51-57.637-25.757-4.886-48.429 2.398-68.468 18.706-11.754 9.566-20.742 21.653-29.963 33.515-7.215 9.283-14.273 18.705-21.855 27.681-6.101 7.223-13.527 12.849-22.954 15.37-19.787 5.29-38.579-5.736-41.277-25.005-3.016-21.536 13.96-40.408 36.739-35.904 7.873 1.557 14.12 6.308 20.005 11.469 3.141 2.753 6.078 5.739 9.299 8.803Z" style="fill:#fff;fill-rule:nonzero" transform="matrix(.81681 0 0 .8192 28.171 86.549)"/><defs><image id="a" width="318" height="318" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAT4AAAE+CAYAAAAUOHwwAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAELUlEQVR4nO3UQRGAQBDAsIXnScU0UsAFN0MTBX31mJlZ1/0MQMS5OwDga8YH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8AAB/9wLC9QTMTO4oPgAAAABJRU5ErkJggg=="/></defs></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

@@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getHumanReadableLocationValue } from "@calcom/core/location";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { defaultHandler, defaultResponder, getTranslation } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
@@ -31,6 +32,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
title: true,
description: true,
customInputs: true,
responses: true,
startTime: true,
endTime: true,
location: true,
@@ -53,6 +55,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
price: true,
currency: true,
length: true,
bookingFields: true,
},
},
attendees: {
@@ -68,7 +71,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const t = await getTranslation(bookings[0].user?.locale ?? "en", "common");
const updatedBookings = bookings.map((booking) => {
return { ...booking, location: getHumanReadableLocationValue(booking.location || "", t) };
return {
...booking,
...getCalEventResponses({
bookingFields: booking.eventType?.bookingFields ?? null,
booking,
}),
location: getHumanReadableLocationValue(booking.location || "", t),
};
});
res.status(201).json(updatedBookings);
-14
View File
@@ -1,14 +0,0 @@
/* @see https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/31#issuecomment-1024722576 */
const tailwind = require("prettier-plugin-tailwindcss");
const sortImports = require("@trivago/prettier-plugin-sort-imports");
const combinedFormatter = {
parsers: {
typescript: {
...tailwind.parsers.typescript,
preprocess: sortImports.parsers.typescript.preprocess,
},
},
};
module.exports = combinedFormatter;
+1
View File
@@ -30,6 +30,7 @@ const config = {
"no",
"uk",
"da",
"tr",
],
},
reloadOnPrerender: process.env.NODE_ENV !== "production",
+3 -3
View File
@@ -20,13 +20,13 @@
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/line-clamp": "^0.4.0",
"@tailwindcss/typography": "^0.5.4",
"@trivago/prettier-plugin-sort-imports": "3.2.0",
"@trivago/prettier-plugin-sort-imports": "4.1.1",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
"eslint": "^8.34.0",
"eslint-plugin-unused-imports": "^2.0.0",
"prettier": "^2.8.4",
"prettier-plugin-tailwindcss": "^0.1.13",
"prettier": "^2.8.6",
"prettier-plugin-tailwindcss": "^0.2.5",
"tailwind-scrollbar": "^2.0.1",
"tailwindcss": "^3.2.1",
"typescript": "^4.9.4"
+8 -1
View File
@@ -16,7 +16,14 @@ module.exports = {
"^[./]",
],
importOrderSeparation: true,
plugins: [require("./merged-prettier-plugin")],
plugins: [
"@trivago/prettier-plugin-sort-imports",
/**
* **NOTE** tailwind plugin must come last!
* @see https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
*/
"prettier-plugin-tailwindcss",
],
overrides: [
{
files: ["apps/website/lib/utils/wordlist/wordlist.ts"],
+4 -1
View File
@@ -77,9 +77,11 @@ export const getConnectedCalendars = async (
},
};
}
if (destinationCalendar) {
// HACK https://github.com/calcom/cal.com/pull/7644/files#r1131508414
if (destinationCalendar && !Object.isFrozen(destinationCalendar)) {
destinationCalendar.primaryEmail = primary.email;
destinationCalendar.integrationTitle = integration.title;
destinationCalendar = Object.freeze(destinationCalendar);
}
return {
@@ -263,6 +265,7 @@ export const createEvent = async (
type: credential.type,
success,
uid,
iCalUID: creationResult?.iCalUID || undefined,
createdEvent: creationResult,
originalEvent: calEvent,
calError,
+16 -7
View File
@@ -10,15 +10,12 @@ import { MeetLocationType } from "@calcom/app-store/locations";
import getApps from "@calcom/app-store/utils";
import prisma from "@calcom/prisma";
import { createdEventSchema } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar";
import type { NewCalendarEventType } from "@calcom/types/Calendar";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
import type { Event } from "@calcom/types/Event";
import type {
CreateUpdateResult,
EventResult,
PartialBooking,
PartialReference,
} from "@calcom/types/EventManager";
import type { EventResult } from "@calcom/types/EventManager";
import type { CreateUpdateResult, PartialBooking, PartialReference } from "@calcom/types/EventManager";
import { createEvent, updateEvent } from "./CalendarManager";
import { createMeeting, updateMeeting } from "./videoClient";
@@ -125,12 +122,24 @@ export default class EventManager {
// Create the calendar event with the proper video call data
results.push(...(await this.createAllCalendarEvents(clonedCalEvent)));
// Since the result can be a new calendar event or video event, we have to create a type guard
// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
const isCalendarResult = (
result: (typeof results)[number]
): result is EventResult<NewCalendarEventType> => {
return result.type.includes("_calendar");
};
const referencesToCreate = results.map((result) => {
let createdEventObj: createdEventSchema | null = null;
if (typeof result?.createdEvent === "string") {
createdEventObj = createdEventSchema.parse(JSON.parse(result.createdEvent));
}
if (isCalendarResult(result)) {
evt.iCalUID = result.iCalUID || undefined;
}
return {
type: result.type,
uid: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString() ?? "",
+20 -1
View File
@@ -1,3 +1,5 @@
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
const Spacer = () => <p style={{ height: 6 }} />;
export const Info = (props: {
@@ -6,8 +8,14 @@ export const Info = (props: {
extraInfo?: React.ReactNode;
withSpacer?: boolean;
lineThrough?: boolean;
formatted?: boolean;
}) => {
if (!props.description || props.description === "") return null;
const descriptionCSS = "color: '#101010'; font-weight: 400; line-height: 24px; margin: 0;";
const safeDescription = markdownToSafeHTML(props.description.toString()) || "";
return (
<>
{props.withSpacer && <Spacer />}
@@ -21,7 +29,18 @@ export const Info = (props: {
whiteSpace: "pre-wrap",
textDecoration: props.lineThrough ? "line-through" : undefined,
}}>
{props.description}
{props.formatted ? (
<p
className="dark:text-darkgray-600 mt-2 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{
__html: safeDescription
.replaceAll("<p>", `<p style="${descriptionCSS}">`)
.replaceAll("<li>", `<li style="${descriptionCSS}">`),
}}
/>
) : (
props.description
)}
</p>
{props.extraInfo}
</div>
@@ -76,7 +76,7 @@ export const BaseScheduledEmail = (
<WhenInfo calEvent={props.calEvent} t={t} timeZone={timeZone} />
<WhoInfo calEvent={props.calEvent} t={t} />
<LocationInfo calEvent={props.calEvent} t={t} />
<Info label={t("description")} description={props.calEvent.description} withSpacer />
<Info label={t("description")} description={props.calEvent.description} withSpacer formatted />
<Info label={t("additional_notes")} description={props.calEvent.additionalNotes} withSpacer />
{props.includeAppsStatus && <AppsStatus calEvent={props.calEvent} t={t} />}
<UserFieldsResponses calEvent={props.calEvent} />
@@ -16,20 +16,18 @@ export default class AttendeeScheduledEmail extends BaseEmail {
attendee: Person;
showAttendees: boolean | undefined;
t: TFunction;
attendees: Person[];
constructor(calEvent: CalendarEvent, attendee: Person, showAttendees?: boolean | undefined) {
super();
this.name = "SEND_BOOKING_CONFIRMATION";
this.calEvent = calEvent;
this.attendee = attendee;
this.showAttendees = showAttendees;
this.t = attendee.language.translate;
this.attendees = [...this.calEvent.attendees];
if (!this.showAttendees && this.calEvent.seatsPerTimeSlot) {
this.attendees = [this.attendee];
this.calEvent.attendees = [this.attendee];
if (!showAttendees && calEvent.seatsPerTimeSlot) {
this.calEvent = cloneDeep(calEvent);
this.calEvent.attendees = [attendee];
} else {
this.calEvent = calEvent;
}
this.name = "SEND_BOOKING_CONFIRMATION";
this.attendee = attendee;
this.t = attendee.language.translate;
}
protected getiCalEventAsString(): string | undefined {
@@ -39,21 +37,21 @@ export default class AttendeeScheduledEmail extends BaseEmail {
// ics appends "RRULE:" already, so removing it from RRule generated string
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
}
const partstat: ParticipationStatus = "NEEDS-ACTION";
const partstat: ParticipationStatus = "ACCEPTED";
const role: ParticipationRole = "REQ-PARTICIPANT";
const icsEvent = createEvent({
uid: this.calEvent.iCalUID || this.calEvent.uid!,
start: dayjs(this.calEvent.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
productId: "calcom/ics",
title: this.calEvent.title,
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
attendees: [
...this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
@@ -47,7 +47,7 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
productId: "calcom/ics",
title: this.t("ics_event_title", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,

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