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

No files matched your search

+6 -6
View File
@@ -12,13 +12,13 @@ Fixes # (issue)
## Type of change ## Type of change
<!-- Please delete options that are not relevant. --> <!-- Please delete bullets that are not relevant. -->
- [ ] Bug fix (non-breaking change which fixes an issue) - Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements) - Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality) - New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update - This change requires a documentation update
## How should this be tested? ## How should this be tested?
+18
View File
@@ -0,0 +1,18 @@
{
"problemMatcher": [
{
"owner": "tsc-absolute",
"pattern": [
{
"regexp": "(?:^|\\s)([^\\s].*)[\\(:](\\d+)[,:](\\d+)(?:\\):\\s+|\\s+-\\s+)(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"code": 5,
"message": 6
}
]
}
]
}
@@ -0,0 +1,27 @@
name: "Apply issue labels to PR"
on:
pull_request:
types: [opened]
jobs:
label_on_pr:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 16
- name: Apply labels from linked issue to PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_DATA: ${{ toJson(github.event.pull_request) }}
run: node ./.github/workflows/scripts/apply-issue-labels-to-pr.ts
+5 -3
View File
@@ -10,8 +10,10 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-install
- name: Show info - name: Show info
run: node -e "console.log(require('v8').getHeapStatistics())" run: node -e "console.log(require('v8').getHeapStatistics())"
- name: Configure TSC problem matcher
- run: yarn type-check run: |
echo "::remove-matcher owner=tsc::"
echo "::add-matcher::.github/matchers/tsc-absolute.json"
- run: yarn type-check:ci
+3 -2
View File
@@ -1,11 +1,12 @@
name: Cron - downgradeUsers name: Cron - downgradeUsers
on: on:
workflow_dispatch:
# "Scheduled workflows run on the latest commit on the default or base branch." # "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule # — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule: schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru) # Runs “Every month at 1st (see https://crontab.guru)
- cron: "0,15,30,45 * * * *" - cron: "0 0 1 * *"
jobs: jobs:
cron-downgradeUsers: cron-downgradeUsers:
env: env:
+1 -1
View File
@@ -26,6 +26,6 @@ jobs:
localization_branch_name: main localization_branch_name: main
create_pull_request: false create_pull_request: false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
@@ -0,0 +1,149 @@
const https = require("https");
async function applyLabelFromLinkedIssueToPR(pr, token) {
// Get the labels from issues linked to the PR
const query = `
query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
closingIssuesReferences(first: 10) {
nodes {
number
labels(first: 10) {
nodes {
name
}
}
}
}
}
}
}
`;
const graphqlData = JSON.stringify({
query,
variables: {
owner: pr.base.repo.owner.login,
repo: pr.base.repo.name,
prNumber: pr.number,
},
});
const requestOptions = {
hostname: "api.github.com",
path: "/graphql",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": graphqlData.length,
"Authorization": "Bearer " + token,
"User-Agent": "Node.js",
},
};
// Use the native Node.js request library to make the request
let linkedIssues;
linkedIssues = await new Promise((resolve, reject) => {
const request = https.request(requestOptions, (response) => {
let responseBody = "";
response.on("data", (chunk) => {
responseBody += chunk;
});
response.on("end", () => {
resolve(
JSON.parse(responseBody)
?.data
?.repository
?.pullRequest
?.closingIssuesReferences
?.nodes
);
});
});
request.on("error", (error) => {
reject(error);
});
request.write(graphqlData);
request.end();
});
if (!linkedIssues || linkedIssues.length === 0) {
console.log("No issue linked.");
return;
}
// Iterate over linked issues and apply labels to PR
for (const issue of linkedIssues) {
const labels = issue?.labels?.nodes?.map((label) => label.name);
if (!labels || labels.length === 0) {
console.log(`No labels found on the linked issue #${issue.number}.`);
continue;
}
const labelsData = JSON.stringify({
labels: labels,
});
const requestOptions = {
hostname: "api.github.com",
path: `/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/issues/${pr.number}/labels`,
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": labelsData.length,
"Authorization": "Bearer " + token,
"User-Agent": "Node.js",
},
};
let labelResult;
labelResult = await new Promise((resolve, reject) => {
const request = https.request(requestOptions, (response) => {
let responseBody = "";
response.on("data", (chunk) => {
responseBody += chunk;
});
response.on("data", () => {});
response.on("end", () => {
resolve(JSON.parse(responseBody));
});
});
request.on("error", (error) => {
reject(error);
});
request.write(labelsData);
request.end();
});
if (labelResult.message) {
console.log(`Error labelling PR: ${labelResult.message}`);
continue;
}
console.log(
`Applied labels: ${labels.join(", ")} to PR #${
pr.number
} from linked issue #${issue.number}`
);
}
}
// Pass the PR data and GitHub token to the script
(async () => {
if (!process.env.PR_DATA) {
console.log("No PR data found.");
return;
}
const prData = JSON.parse(process.env.PR_DATA);
const token = process.env.GITHUB_TOKEN;
await applyLabelFromLinkedIssueToPR(prData, token);
})();
+4 -5
View File
@@ -350,8 +350,7 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
<source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=dark"> <source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=dark">
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light"> <img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
</picture> </picture>
</a> </a>
<!-- CONTRIBUTORS --> <!-- CONTRIBUTORS -->
@@ -456,12 +455,12 @@ following
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one. 1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
2. From within the API console page, go to "Applications". 2. From within the API console page, go to "Applications".
3. Click "ADD CLIENT" button top right and select "Server-based Applications". 3. Click "ADD CLIENT" button top right and select "Server-based Applications".
4. Fill in any information you want in the "Client Details" tab 4. Fill in any information you want in the "Client Details" tab
5. Go to tab "Client Secret" tab. 5. Go to tab "Client Secret" tab.
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields. 6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs. 7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers. 8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
9. Click the "Save"/ "UPDATE" button at the bottom footer. 9. Click the "Save"/ "UPDATE" button at the bottom footer.
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings. 10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
+2 -1
View File
@@ -11,7 +11,8 @@
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix", "lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json", "lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
"start": "PORT=4000 next start", "start": "PORT=4000 next start",
"type-check": "tsc --pretty --noEmit" "type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit"
}, },
"author": "Cal.com, Inc.", "author": "Cal.com, Inc.",
"license": "MIT", "license": "MIT",
@@ -1,86 +0,0 @@
import React from "react";
import Select from "react-select";
import type { OptionProps } from "react-select";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App";
import { Button } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
interface AdditionalCalendarSelectorProps {
isLoading?: boolean;
}
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
const { data } = optionProps;
return (
<InstallAppButton
type={data.type}
render={(installProps) => {
return (
<Button {...installProps} className="w-full" color="minimal">
{/* eslint-disable @next/next/no-img-element */}
{data.image && (
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
)}
<p>{data.label}</p>
</Button>
);
}}
/>
);
};
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
return (
<QueryCell
query={query}
success={({ data }) => {
const options = data.items.map((item) => ({
label: item.name,
slug: item.slug,
image: item.logo,
type: item.type,
}));
return (
<Select
name="additionalCalendar"
placeholder={t("connect_additional_calendar")}
options={options}
styles={{
placeholder: (defaultStyles) => {
return {
...defaultStyles,
color: "#3E3E3E",
marginLeft: "3px",
};
},
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "2px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
maxWidth: "320px",
},
};
},
}}
isSearchable={false}
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm font-medium text-gray-700"
isLoading={isLoading}
components={{ Option: ImageOption }}
/>
);
}}
/>
);
};
export default AdditionalCalendarSelector;
@@ -1,137 +0,0 @@
import classNames from "classnames";
import React, { useEffect, useState } from "react";
import Select from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { DestinationCalendar } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
interface Props {
onChange: (value: { externalId: string; integration: string }) => void;
isLoading?: boolean;
hidePlaceholder?: boolean;
/** The external Id of the connected calendar */
value: string | undefined;
destinationCalendar?: DestinationCalendar | null;
}
const DestinationCalendarSelector = ({
onChange,
isLoading,
value,
hidePlaceholder,
destinationCalendar,
}: Props): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.connectedCalendars.useQuery();
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
// Extra styles to show prefixed text in react-select
const content = (hidePlaceholder = false) => {
if (!hidePlaceholder) {
return {
alignItems: "center",
display: "flex",
":before": {
content: `'${t("select_destination_calendar")}:'`,
display: "block",
marginRight: 8,
},
};
}
return {};
};
useEffect(() => {
const selected = query.data?.connectedCalendars
.map((connected) => connected.calendars ?? [])
.flat()
.find((cal) => cal.externalId === value);
if (selected) {
setSelectedOption({
value: `${selected.integration}:${selected.externalId}`,
label: selected.name || "",
});
}
}, [query.data?.connectedCalendars, value]);
if (!query.data?.connectedCalendars.length) {
return null;
}
const options =
query.data.connectedCalendars.map((selectedCalendar) => ({
key: selectedCalendar.credentialId,
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
options: (selectedCalendar.calendars ?? [])
.filter((cal) => cal.readOnly === false)
.map((cal) => ({
label: cal.name || "",
value: `${cal.integration}:${cal.externalId}`,
})),
})) ?? [];
const defaultCalendarSelectedString = destinationCalendar?.externalId
? `(${
destinationCalendar.externalId.length > 15
? destinationCalendar.externalId.substring(0, 15) + "..."
: destinationCalendar.externalId
})`
: "";
return (
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
<Select
name="primarySelectedCalendar"
placeholder={
!hidePlaceholder ? (
`${t("select_destination_calendar")}`
) : (
<span className="whitespace-nowrap">
{t("default_calendar_selected")} {defaultCalendarSelectedString}
</span>
)
}
options={options}
styles={{
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "6px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
width: "100%",
},
};
},
}}
isSearchable={false}
className={classNames(
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
!hidePlaceholder && "font-medium"
)}
onChange={(option) => {
setSelectedOption(option);
if (!option) {
return;
}
/* Split only the first `:`, since Apple uses the full URL as externalId */
const [integration, externalId] = option.value.split(/:(.+)/);
onChange({
integration,
externalId,
});
}}
isLoading={isLoading}
value={selectedOption}
components={{
IndicatorSeparator: () => null,
}}
/>
</div>
);
};
export default DestinationCalendarSelector;
+74 -87
View File
@@ -1,4 +1,4 @@
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
import classNames from "classnames"; import classNames from "classnames";
import type { NextRouter } from "next/router"; import type { NextRouter } from "next/router";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -22,7 +22,7 @@ import {
TextField, TextField,
ColorPicker, ColorPicker,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon"; import { FiCode, FiTrello, FiSun, FiArrowLeft } from "@calcom/ui/components/icon";
import Select from "@components/ui/form/Select"; import Select from "@components/ui/form/Select";
@@ -167,7 +167,7 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
const height = getDimension(previewState.inline.height); const height = getDimension(previewState.inline.height);
return code` return code`
import Cal, { getCalApi } from "@calcom/embed-react"; import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function MyComponent() { function MyComponent() {
useEffect(()=>{ useEffect(()=>{
(async function () { (async function () {
@@ -187,12 +187,12 @@ function MyComponent() {
}) => { }) => {
return code` return code`
import Cal, { getCalApi } from "@calcom/embed-react"; import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function MyComponent() { function MyComponent() {
useEffect(()=>{ useEffect(()=>{
(async function () { (async function () {
const cal = await getCalApi(); const cal = await getCalApi();
Cal("floatingButton", ${floatingButtonArg}); cal("floatingButton", ${floatingButtonArg});
${uiInstructionCode} ${uiInstructionCode}
})(); })();
}, []) }, [])
@@ -201,7 +201,7 @@ function MyComponent() {
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => { "element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
return code` return code`
import Cal, { getCalApi } from "@calcom/embed-react"; import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function MyComponent() { function MyComponent() {
useEffect(()=>{ useEffect(()=>{
(async function () { (async function () {
@@ -313,7 +313,7 @@ const embeds: {
}[] = [ }[] = [
{ {
title: "Inline Embed", title: "Inline Embed",
subtitle: "Loads your Cal scheduling page directly inline with your other website content", subtitle: "Loads your event type directly inline with your other website content.",
type: "inline", type: "inline",
illustration: ( illustration: (
<svg <svg
@@ -798,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
} }
const ThemeOptions = [ const ThemeOptions = [
{ value: Theme.auto, label: "Auto Theme" }, { value: Theme.auto, label: "Auto" },
{ value: Theme.dark, label: "Dark Theme" }, { value: Theme.dark, label: "Dark Theme" },
{ value: Theme.light, label: "Light Theme" }, { value: Theme.light, label: "Light Theme" },
]; ];
@@ -815,11 +815,18 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
]; ];
return ( return (
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation"> <DialogContent
ref={dialogContentRef}
className="rounded-lg p-0.5 sm:max-w-[80rem]"
enableOverflow
type="creation">
<div className="flex"> <div className="flex">
<div className="flex w-1/3 flex-col bg-gray-50 p-8"> <div className="flex w-1/3 flex-col bg-gray-50 p-8">
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title"> <h3
className="mb-2.5 flex items-center text-xl font-semibold leading-5 text-gray-900"
id="modal-title">
<button <button
className="h-6 w-6"
onClick={() => { onClick={() => {
removeQueryParams(router, ["embedType", "embedTabName"]); removeQueryParams(router, ["embedType", "embedTabName"]);
}}> }}>
@@ -827,55 +834,43 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</button> </button>
{embed.title} {embed.title}
</h3> </h3>
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} /> <h4 className="mb-6 text-sm font-normal text-gray-500">{embed.subtitle}</h4>
<div className="flex flex-col overflow-y-auto"> <div className="flex flex-col">
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}> <div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible <Collapsible
open={isEmbedCustomizationOpen} open={isEmbedCustomizationOpen}
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}> onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
<CollapsibleTrigger
type="button"
className="flex w-full items-center text-base font-medium text-gray-900">
<div>
{embedType === "inline"
? "Inline Embed Customization"
: embedType === "floating-popup"
? "Floating Popup Customization"
: "Element Click Customization"}
</div>
<FiChevronRight
className={`${
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-gray-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent className="text-sm"> <CollapsibleContent className="text-sm">
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}> <div className={classNames(embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */} {/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-sm">Embed Window Sizing</div> <div className="mb-[9px] text-sm">Window sizing</div>
<div className="justify-left flex items-center"> <div className="justify-left flex items-center !font-normal">
<TextField <div className="mr-[9px]">
labelProps={{ className: "hidden" }} <TextField
required labelProps={{ className: "hidden" }}
value={previewState.inline.width} className="focus:ring-offset-0"
onChange={(e) => { required
setPreviewState((previewState) => { value={previewState.inline.width}
const width = e.target.value || "100%"; onChange={(e) => {
setPreviewState((previewState) => {
const width = e.target.value || "100%";
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<>W</>}
/>
</div>
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<>W</>}
/>
<span className="p-2">×</span>
<TextField <TextField
labelProps={{ className: "hidden" }} labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
value={previewState.inline.height} value={previewState.inline.height}
required required
onChange={(e) => { onChange={(e) => {
@@ -1007,22 +1002,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
</div> </div>
<hr className="mt-4" /> <div className="font-medium">
<div className="mt-4 font-medium">
<Collapsible <Collapsible
open={isBookingCustomizationOpen} open={isBookingCustomizationOpen}
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}> onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleTrigger className="flex w-full" type="button">
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
<FiChevronRight
className={`${
isBookingCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-gray-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="mt-6 text-sm"> <div className="mt-6 text-sm">
<div className="mb-4 flex items-center justify-start space-x-2 rtl:space-x-reverse"> <div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
<Switch <Switch
checked={previewState.hideEventTypeDetails} checked={previewState.hideEventTypeDetails}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
@@ -1036,13 +1022,38 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
/> />
<div className="text-sm">{t("hide_eventtype_details")}</div> <div className="text-sm">{t("hide_eventtype_details")}</div>
</div> </div>
<Label className=""> {[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="mb-6">
<div className="mb-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
<Label>
<div className="mb-2">Theme</div> <div className="mb-2">Theme</div>
<Select <Select
className="w-full" className="w-full"
defaultValue={ThemeOptions[0]} defaultValue={ThemeOptions[0]}
components={{ components={{
Control: ThemeSelectControl, Control: ThemeSelectControl,
IndicatorSeparator: () => null,
}} }}
onChange={(option) => { onChange={(option) => {
if (!option) { if (!option) {
@@ -1058,30 +1069,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
options={ThemeOptions} options={ThemeOptions}
/> />
</Label> </Label>
{[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="pb-4">
<div className="mb-2 pt-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
@@ -1,10 +1,14 @@
import type { OptionProps } from "react-select";
import { InstallAppButton } from "@calcom/app-store/components"; import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App"; import {
import { Button, Select } from "@calcom/ui"; Button,
Dropdown,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
DropdownItem,
} from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon"; import { FiPlus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
@@ -13,31 +17,6 @@ interface AdditionalCalendarSelectorProps {
isLoading?: boolean; isLoading?: boolean;
} }
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
const { t } = useLocale();
const { data } = optionProps;
return data.slug !== "add-new" ? (
<InstallAppButton
type={data.type}
render={(installProps) => {
return (
<Button {...installProps} className="flex w-full align-top" color="minimal">
{data.image && (
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
)}
<p className="text-left">{`${t("add")} ${data.label}`}</p>
</Button>
);
}}
/>
) : (
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
<FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
<p>{t("install_new_calendar_app")}</p>
</Button>
);
};
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => { const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale(); const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true }); const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
@@ -59,51 +38,39 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
type: "new_other", type: "new_other",
}); });
return ( return (
<Select <Dropdown modal={false}>
name="additionalCalendar" <DropdownMenuTrigger asChild>
placeholder={ <Button StartIcon={FiPlus} color="secondary" {...(isLoading && { loading: isLoading })}>
<Button StartIcon={FiPlus} color="secondary">
{t("add")} {t("add")}
</Button> </Button>
} </DropdownMenuTrigger>
options={options} <DropdownMenuContent>
isSearchable={false} {options.map((data) => (
isLoading={isLoading} <DropdownMenuItem key={data.slug} className="focus:outline-none">
components={{ Option: ImageOption }} {data.slug === "add-new" ? (
styles={{ <DropdownItem StartIcon={FiPlus} color="minimal" href="/apps/categories/calendar">
menu: (defaultStyles) => ({ {t("install_new_calendar_app")}
...defaultStyles, </DropdownItem>
"@media only screen and (max-width: 640px)": { ) : (
left: "0", <InstallAppButton
}, type={data.type}
width: "max-content", render={(installProps) => {
right: "0", const props = { ...installProps } as any;
}), return (
indicatorSeparator: (defaultStyles) => ({ <DropdownItem {...props} color="minimal" type="button">
...defaultStyles, <span className="flex items-center gap-x-2">
display: "none", {data.image && <img className="h-5 w-5" src={data.image} alt={data.label} />}
}), {`${t("add")} ${data.label}`}
control: (defaultStyles) => ({ </span>
...defaultStyles, </DropdownItem>
padding: "0", );
border: "0", }}
borderRadius: "6px", />
minHeight: "auto", )}
}), </DropdownMenuItem>
dropdownIndicator: (defaultStyles) => ({ ))}
...defaultStyles, </DropdownMenuContent>
display: "none", </Dropdown>
}),
valueContainer: (defaultStyles) => ({
...defaultStyles,
padding: "0",
}),
placeholder: (defaultStyles) => ({
...defaultStyles,
margin: "0",
}),
}}
/>
); );
}} }}
/> />
@@ -1,139 +0,0 @@
import classNames from "classnames";
import React, { useEffect, useState } from "react";
import Select from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { DestinationCalendar } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
interface Props {
onChange: (value: { externalId: string; integration: string }) => void;
isLoading?: boolean;
hidePlaceholder?: boolean;
/** The external Id of the connected calendar */
value: string | undefined;
destinationCalendar?: DestinationCalendar | null;
}
const DestinationCalendarSelector = ({
onChange,
isLoading,
value,
hidePlaceholder,
destinationCalendar,
}: Props): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.connectedCalendars.useQuery();
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
// Extra styles to show prefixed text in react-select
const content = (hidePlaceholder = false) => {
if (!hidePlaceholder) {
return {
alignItems: "center",
display: "flex",
":before": {
content: `'${t("select_destination_calendar")}:'`,
display: "block",
marginRight: 8,
},
};
}
return {};
};
useEffect(() => {
const selected = query.data?.connectedCalendars
.map((connected) => connected.calendars ?? [])
.flat()
.find((cal) => cal.externalId === value);
if (selected) {
setSelectedOption({
value: `${selected.integration}:${selected.externalId}`,
label: `${selected.name} (${selected.externalId})` || "",
});
}
}, [query.data?.connectedCalendars, value]);
if (!query.data?.connectedCalendars.length) {
return null;
}
const options =
query.data.connectedCalendars.map((selectedCalendar) => ({
key: selectedCalendar.credentialId,
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
options: (selectedCalendar.calendars ?? [])
.filter((cal) => cal.readOnly === false)
.map((cal) => ({
label: `${cal.name} (${cal.externalId})` || "",
value: `${cal.integration}:${cal.externalId}`,
})),
})) ?? [];
const defaultCalendarSelectedString = destinationCalendar?.externalId
? `(${
destinationCalendar.externalId.length > 15
? destinationCalendar.externalId.substring(0, 15) + "..."
: destinationCalendar.externalId
})`
: "";
return (
<div
className="relative flex items-center"
title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
<Select
name="primarySelectedCalendar"
placeholder={
!hidePlaceholder ? (
`${t("select_destination_calendar")}`
) : (
<span className="whitespace-nowrap">
{t("default_calendar_selected")} {defaultCalendarSelectedString}
</span>
)
}
options={options}
styles={{
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "6px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
width: "100%",
},
};
},
}}
isSearchable={false}
className={classNames(
"block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
!hidePlaceholder && "font-medium"
)}
onChange={(option) => {
setSelectedOption(option);
if (!option) {
return;
}
/* Split only the first `:`, since Apple uses the full URL as externalId */
const [integration, externalId] = option.value.split(/:(.+)/);
onChange({
integration,
externalId,
});
}}
isLoading={isLoading}
value={selectedOption}
components={{
IndicatorSeparator: () => null,
}}
/>
</div>
);
};
export default DestinationCalendarSelector;
@@ -8,54 +8,61 @@ import { FiLink } from "@calcom/ui/components/icon";
import type { Props } from "./pages/AvailabilityPage"; import type { Props } from "./pages/AvailabilityPage";
const excludeNullValues = (value: unknown) => !!value;
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) { export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
const { t } = useLocale(); const { t } = useLocale();
return locations.length ? ( const renderLocations = locations.map((location, index) => {
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600"> const eventLocationType = getEventLocationType(location.type);
{locations.map((location, index) => { if (!eventLocationType) {
const eventLocationType = getEventLocationType(location.type); // It's possible that the location app got uninstalled
if (!eventLocationType) { return null;
// It's possible that the location app got uninstalled }
return null; if (eventLocationType.variable === "hostDefault") {
} return null;
}
const translateAbleKeys = [ const translateAbleKeys = [
"attendee_in_person", "attendee_in_person",
"in_person", "in_person",
"attendee_phone_number", "attendee_phone_number",
"link_meeting", "link_meeting",
"organizer_phone_number", "organizer_phone_number",
]; ];
const locationKey = z.string().default("").parse(locationKeyToString(location)); const locationKey = z.string().default("").parse(locationKeyToString(location));
const translatedLocation = location.type.startsWith("integrations:")
? eventLocationType.label
: translateAbleKeys.includes(locationKey)
? t(locationKey)
: locationKey;
const translatedLocation = location.type.startsWith("integrations:") return (
? eventLocationType.label <div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
: translateAbleKeys.includes(locationKey) {eventLocationType.iconUrl === "/link.svg" ? (
? t(locationKey) <FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
: locationKey; ) : (
<img
return ( src={eventLocationType.iconUrl}
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium"> className={classNames(
{eventLocationType.iconUrl === "/link.svg" ? ( "ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " /> !eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
) : (
<img
src={eventLocationType.iconUrl}
className={classNames(
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
)}
alt={`${eventLocationType.label} icon`}
/>
)} )}
<Tooltip content={translatedLocation}> alt={`${eventLocationType.label} icon`}
<p className="line-clamp-1">{translatedLocation}</p> />
</Tooltip> )}
</div> <Tooltip content={translatedLocation}>
); <p className="line-clamp-1">{translatedLocation}</p>
})} </Tooltip>
</div>
);
});
const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[];
return filteredLocations.length ? (
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
{filteredLocations}
</div> </div>
) : null; ) : null;
} }
@@ -33,6 +33,7 @@ interface ISetLocationDialog {
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void; saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
selection?: LocationOption; selection?: LocationOption;
booking?: BookingItem; booking?: BookingItem;
isTeamEvent?: boolean;
defaultValues?: LocationObject[]; defaultValues?: LocationObject[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>; setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean; isOpenDialog: boolean;
@@ -74,6 +75,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
saveLocation, saveLocation,
selection, selection,
booking, booking,
isTeamEvent,
setShowLocationModal, setShowLocationModal,
isOpenDialog, isOpenDialog,
defaultValues, defaultValues,
@@ -290,7 +292,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
query={locationsQuery} query={locationsQuery}
success={({ data }) => { success={({ data }) => {
if (!data.length) return null; if (!data.length) return null;
const locationOptions = [...data]; const locationOptions = [...data].filter((option) => {
return !isTeamEvent ? option.label !== "Conferencing" : true;
});
if (booking) { if (booking) {
locationOptions.map((location) => locationOptions.map((location) =>
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value)) location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
@@ -10,11 +10,12 @@ import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } from "@calcom/core/event"; import { getEventName } from "@calcom/core/event";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector"; import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder"; import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants"; import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client"; import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui"; import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
import { FiEdit, FiCopy } from "@calcom/ui/components/icon"; import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController"; import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -183,9 +184,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
setRedirectUrlVisible(e); setRedirectUrlVisible(e);
onChange(e ? value : ""); onChange(e ? value : "");
}}> }}>
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */} {/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
<div className="lg:-ml-2"> <div className="lg:-ml-2 lg:-mb-2">
<TextField <TextField
className="w-full"
label={t("redirect_success_booking")} label={t("redirect_success_booking")}
labelSrOnly labelSrOnly
placeholder={t("external_redirect_url")} placeholder={t("external_redirect_url")}
@@ -194,21 +196,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
defaultValue={eventType.successRedirectUrl || ""} defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")} {...formMethods.register("successRedirectUrl")}
/> />
<div className="mt-2 flex"> <div
<Checkbox className={classNames(
description={t("disable_success_page")} "p-1 text-sm text-orange-600",
// Disable if it's not Self Hosted or if the redirect url is not set formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")} )}>
{...formMethods.register("metadata.disableSuccessPage")} {t("redirect_url_warning")}
/>
{/*TODO: Extract it out into a component when used more than once*/}
{!IS_SELF_HOSTED && (
<Link href="https://cal.com/pricing" target="_blank">
<Badge variant="orange" className="ml-2">
Platform Only
</Badge>
</Link>
)}
</div> </div>
</div> </div>
</SettingsToggle> </SettingsToggle>
@@ -426,6 +426,7 @@ const IntervalLimitItem = ({
required required
type="number" type="number"
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"} containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
className="mb-0"
placeholder={`${value}`} placeholder={`${value}`}
min={step} min={step}
step={step} step={step}
+44 -25
View File
@@ -4,7 +4,7 @@ import { isValidPhoneNumber } from "libphonenumber-js";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import Link from "next/link"; import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form"; import { Controller, useForm, useFormContext } from "react-hook-form";
import type { MultiValue } from "react-select"; import type { MultiValue } from "react-select";
import { z } from "zod"; import { z } from "zod";
@@ -16,7 +16,17 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt"; import { md } from "@calcom/lib/markdownIt";
import { slugify } from "@calcom/lib/slugify"; import { slugify } from "@calcom/lib/slugify";
import turndown from "@calcom/lib/turndownService"; import turndown from "@calcom/lib/turndownService";
import { Button, Editor, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui"; import {
Button,
Label,
Select,
SettingsToggle,
Skeleton,
TextField,
Editor,
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon"; import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
@@ -35,14 +45,31 @@ const getLocationFromType = (
} }
}; };
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => { interface DescriptionEditorProps {
for (const locationType of options) { description?: string | null;
for (const location of locationType.options) { }
if (location.value === type && location.disabled === false) {
return location; const DescriptionEditor = (props: DescriptionEditorProps) => {
} const formMethods = useFormContext<FormValues>();
} const [mounted, setIsMounted] = useState(false);
} const { t } = useLocale();
const { description } = props;
useEffect(() => {
setIsMounted(true);
}, []);
return mounted ? (
<Editor
getText={() => md.render(formMethods.getValues("description") || description || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
/>
) : (
<SkeletonContainer>
<SkeletonText className="block h-24 w-full" />
</SkeletonContainer>
);
}; };
export const EventSetupTab = ( export const EventSetupTab = (
@@ -53,12 +80,16 @@ export const EventSetupTab = (
) => { ) => {
const { t } = useLocale(); const { t } = useLocale();
const formMethods = useFormContext<FormValues>(); const formMethods = useFormContext<FormValues>();
const { eventType, locationOptions, team, destinationCalendar } = props; const { eventType, team, destinationCalendar } = props;
const [showLocationModal, setShowLocationModal] = useState(false); const [showLocationModal, setShowLocationModal] = useState(false);
const [editingLocationType, setEditingLocationType] = useState<string>(""); const [editingLocationType, setEditingLocationType] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined); const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration); const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
const locationOptions = props.locationOptions.filter((option) => {
return !team ? option.label !== "Conferencing" : true;
});
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({ const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
value: mins, value: mins,
label: t("multiple_duration_mins", { count: mins }), label: t("multiple_duration_mins", { count: mins }),
@@ -198,14 +229,6 @@ export const EventSetupTab = (
return ( return (
<li <li
onClick={() => {
locationFormMethods.setValue("locationType", location.type);
locationFormMethods.unregister("locationLink");
locationFormMethods.unregister("locationAddress");
locationFormMethods.unregister("locationPhoneNumber");
setEditingLocationType(location.type);
openLocationModal(location.type);
}}
key={`${location.type}${index}`} key={`${location.type}${index}`}
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer"> className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -287,12 +310,7 @@ export const EventSetupTab = (
/> />
<div> <div>
<Label>{t("description")}</Label> <Label>{t("description")}</Label>
<Editor <DescriptionEditor description={eventType?.description} />
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
/>
</div> </div>
<TextField <TextField
required required
@@ -409,6 +427,7 @@ export const EventSetupTab = (
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */} {/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
<EditLocationDialog <EditLocationDialog
isTeamEvent={!!team}
isOpenDialog={showLocationModal} isOpenDialog={showLocationModal}
setShowLocationModal={setShowLocationModal} setShowLocationModal={setShowLocationModal}
saveLocation={saveLocation} saveLocation={saveLocation}
@@ -1,11 +1,11 @@
export type EventTypeDescriptionSafeProps = { export type EventTypeDescriptionSafeProps = {
eventType: { description: string | null }; eventType: { description: string | null; descriptionAsSafeHTML: string | null };
}; };
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => { export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true }; const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
// @ts-expect-error: @see packages/prisma/middleware/eventTypeDescriptionParseAndSanitize.ts if (eventType.description)
if (eventType.description) props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML }; props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML || "" };
return <div {...props} />; return <div {...props} />;
}; };
@@ -1,9 +1,8 @@
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterInputs } from "@calcom/trpc/react"; import type { RouterInputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
interface ICreateEventsOnCalendarSelectProps { interface ICreateEventsOnCalendarSelectProps {
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null; calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
} }
+6 -5
View File
@@ -2,13 +2,14 @@ import Link from "next/link";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams"; import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { Avatar } from "@calcom/ui"; import { Avatar } from "@calcom/ui";
type TeamType = NonNullable<TeamWithMembers>; type TeamType = NonNullable<TeamWithMembers>;
type MembersType = TeamType["members"]; type MembersType = TeamType["members"];
type MemberType = MembersType[number]; type MemberType = MembersType[number] & { safeBio: string | null };
type TeamTypeWithSafeHtml = Omit<TeamType, "members"> & { members: MemberType[] };
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => { const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
const { t } = useLocale(); const { t } = useLocale();
@@ -30,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
<> <>
<div <div
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600" className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }} dangerouslySetInnerHTML={{ __html: member.safeBio || "" }}
/> />
</> </>
) : ( ) : (
@@ -43,7 +44,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
); );
}; };
const Members = ({ members, teamName }: { members: MembersType; teamName: string | null }) => { const Members = ({ members, teamName }: { members: MemberType[]; teamName: string | null }) => {
if (!members || members.length === 0) { if (!members || members.length === 0) {
return null; return null;
} }
@@ -57,7 +58,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
); );
}; };
const Team = ({ team }: { team: TeamType }) => { const Team = ({ team }: { team: TeamTypeWithSafeHtml }) => {
return ( return (
<div> <div>
<Members members={team.members} teamName={team.name} /> <Members members={team.members} teamName={team.name} />
+5 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "2.7.5", "version": "2.7.8",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
@@ -11,6 +11,7 @@
"dx": "yarn dev", "dx": "yarn dev",
"test-codegen": "yarn playwright codegen http://localhost:3000", "test-codegen": "yarn playwright codegen http://localhost:3000",
"type-check": "tsc --pretty --noEmit", "type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint . --ignore-path .gitignore", "lint": "eslint . --ignore-path .gitignore",
@@ -64,7 +65,7 @@
"@types/turndown": "^5.0.1", "@types/turndown": "^5.0.1",
"@vercel/edge-config": "^0.1.1", "@vercel/edge-config": "^0.1.1",
"@vercel/edge-functions-ui": "^0.2.1", "@vercel/edge-functions-ui": "^0.2.1",
"@vercel/og": "^0.0.21", "@vercel/og": "^0.5.0",
"accept-language-parser": "^1.5.0", "accept-language-parser": "^1.5.0",
"async": "^3.2.4", "async": "^3.2.4",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
@@ -119,6 +120,7 @@
"react-window": "^1.8.7", "react-window": "^1.8.7",
"remark": "^14.0.2", "remark": "^14.0.2",
"rrule": "^2.7.1", "rrule": "^2.7.1",
"sanitize-html": "^2.10.0",
"schema-dts": "^1.1.0", "schema-dts": "^1.1.0",
"short-uuid": "^4.2.0", "short-uuid": "^4.2.0",
"strip-markdown": "^5.0.0", "strip-markdown": "^5.0.0",
@@ -156,6 +158,7 @@
"@types/react-phone-number-input": "^3.0.14", "@types/react-phone-number-input": "^3.0.14",
"@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5", "@types/react-window": "^1.8.5",
"@types/sanitize-html": "^2.9.0",
"@types/stripe": "^8.0.417", "@types/stripe": "^8.0.417",
"@types/uuid": "8.3.1", "@types/uuid": "8.3.1",
"autoprefixer": "^10.4.12", "autoprefixer": "^10.4.12",
+42 -5
View File
@@ -3,10 +3,10 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants"; import { DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui"; import { HeadSeo } from "@calcom/ui";
import { FiFileText, FiCheck, FiBookOpen, FiChevronRight } from "@calcom/ui/components/icon"; import { FiBookOpen, FiCheck, FiChevronRight, FiFileText } from "@calcom/ui/components/icon";
import { ssgInit } from "@server/lib/ssg"; import { ssgInit } from "@server/lib/ssg";
@@ -40,6 +40,43 @@ export default function Custom404() {
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage; const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
const isSignup = router.asPath.startsWith("/signup"); const isSignup = router.asPath.startsWith("/signup");
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com"; const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
/**
* If we're on 404 and the route is insights it means it is disabled
* TODO: Abstract this for all disabled features
**/
const isInsights = router.asPath.startsWith("/insights");
if (isInsights) {
return (
<>
<HeadSeo
title="Feature is currently disabled"
description={t("404_page_not_found")}
nextSeoProps={{
nofollow: true,
noindex: true,
}}
/>
<div className="min-h-screen bg-white px-4" data-testid="404-page">
<main className="mx-auto max-w-xl pt-16 pb-6 sm:pt-24">
<div className="text-center">
<p className="text-sm font-semibold uppercase tracking-wide text-black">{t("error_404")}</p>
<h1 className="font-cal mt-2 text-4xl font-extrabold text-gray-900 sm:text-5xl">
Feature is currently disabled
</h1>
</div>
<div className="mt-12">
<div className="mt-8">
<Link href="/" className="text-base font-medium text-black hover:text-gray-500">
{t("or_go_back_home")}
<span aria-hidden="true"> &rarr;</span>
</Link>
</div>
</div>
</main>
</div>
</>
);
}
return ( return (
<> <>
@@ -62,7 +99,7 @@ export default function Custom404() {
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900"> <h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900">
{t("signup_requires")} {t("signup_requires")}
</h1> </h1>
<p className="mt-4">{t("signup_requires_description", { companyName: COMPANY_NAME })}</p> <p className="mt-4">{t("signup_requires_description", { companyName: "Cal.com" })}</p>
</div> </div>
<div className="mt-12"> <div className="mt-12">
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500"> <h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
@@ -99,7 +136,7 @@ export default function Custom404() {
<ul role="list" className="divide-y divide-gray-200 border-gray-200"> <ul role="list" className="divide-y divide-gray-200 border-gray-200">
<li className="px-4 py-2"> <li className="px-4 py-2">
<Link <Link
href={`${DEVELOPER_DOCS}/self-hosting/installation`} href="https://cal.com/self-hosting/installation"
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50"> <span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
@@ -168,7 +205,7 @@ export default function Custom404() {
</ul> </ul>
<div className="mt-8"> <div className="mt-8">
<Link <Link
href={`${WEBSITE_URL}/enterprise`} href="https://cal.com/sales"
className="text-base font-medium text-black hover:text-gray-500"> className="text-base font-medium text-black hover:text-gray-500">
{t("contact_sales")} {t("contact_sales")}
<span aria-hidden="true"> &rarr;</span> <span aria-hidden="true"> &rarr;</span>
+6 -2
View File
@@ -24,7 +24,7 @@ import defaultEvents, {
} from "@calcom/lib/defaultEvents"; } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
import { md } from "@calcom/lib/markdownIt"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects"; import { baseEventTypeSelect } from "@calcom/prisma/selects";
@@ -147,7 +147,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<> <>
<div <div
className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600" className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(user.bio || "") }} dangerouslySetInnerHTML={{ __html: props.safeBio }}
/> />
</> </>
)} )}
@@ -343,6 +343,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const eventTypes = eventTypesRaw.map((eventType) => ({ const eventTypes = eventTypesRaw.map((eventType) => ({
...eventType, ...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}), metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
})); }));
const isSingleUser = users.length === 1; const isSingleUser = users.length === 1;
@@ -352,9 +353,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}) })
: []; : [];
const safeBio = markdownToSafeHTML(user.bio) || "";
return { return {
props: { props: {
users, users,
safeBio,
profile, profile,
user: { user: {
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"), emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
+2 -6
View File
@@ -5,7 +5,7 @@ import type { LocationObject } from "@calcom/app-store/locations";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata"; import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { addListFormatting } from "@calcom/lib/markdownIt"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { User } from "@calcom/prisma/client"; import type { User } from "@calcom/prisma/client";
import { isBrandingHidden } from "@lib/isBrandingHidden"; import { isBrandingHidden } from "@lib/isBrandingHidden";
@@ -59,7 +59,6 @@ Type.isThemeSupported = true;
const paramsSchema = z.object({ type: z.string(), user: z.string() }); const paramsSchema = z.object({ type: z.string(), user: z.string() });
async function getUserPageProps(context: GetStaticPropsContext) { async function getUserPageProps(context: GetStaticPropsContext) {
// load server side dependencies // load server side dependencies
const MarkdownIt = await import("markdown-it").then((mod) => mod.default);
const prisma = await import("@calcom/prisma").then((mod) => mod.default); const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const { privacyFilteredLocations } = await import("@calcom/app-store/locations"); const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent"); const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
@@ -124,9 +123,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
}, },
}, },
}); });
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
if (!user || !user.eventTypes.length) return { notFound: true }; if (!user || !user.eventTypes.length) return { notFound: true };
const [eventType]: ((typeof user.eventTypes)[number] & { const [eventType]: ((typeof user.eventTypes)[number] & {
@@ -153,7 +149,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}), metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
recurringEvent: parseRecurringEvent(eventType.recurringEvent), recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: privacyFilteredLocations(locations), locations: privacyFilteredLocations(locations),
descriptionAsSafeHTML: eventType.description ? addListFormatting(md.render(eventType.description)) : null, descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}); });
// Check if the user you are logging into has any active teams or premium user name // Check if the user you are logging into has any active teams or premium user name
const hasActiveTeam = const hasActiveTeam =
+4 -2
View File
@@ -13,7 +13,10 @@ import {
getGroupName, getGroupName,
getUsernameList, getUsernameList,
} from "@calcom/lib/defaultEvents"; } from "@calcom/lib/defaultEvents";
import getBooking from "@calcom/lib/getBooking";
import type { GetBookingType } from "@calcom/lib/getBooking";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma, { bookEventTypeSelect } from "@calcom/prisma"; import prisma, { bookEventTypeSelect } from "@calcom/prisma";
import { import {
customInputSchema, customInputSchema,
@@ -21,8 +24,6 @@ import {
userMetadata as userMetadataSchema, userMetadata as userMetadataSchema,
} from "@calcom/prisma/zod-utils"; } from "@calcom/prisma/zod-utils";
import type { GetBookingType } from "@lib/getBooking";
import getBooking from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage"; import BookingPage from "@components/booking/pages/BookingPage";
@@ -189,6 +190,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
slug: u.username, slug: u.username,
theme: u.theme, theme: u.theme,
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}; };
})[0]; })[0];
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { sendOrganizerRequestReminderEmail } from "@calcom/emails"; import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getTranslation } from "@calcom/lib/server/i18n"; import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import prisma, { bookingMinimalSelect } from "@calcom/prisma";
@@ -48,8 +49,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
eventType: { eventType: {
select: { select: {
recurringEvent: true, recurringEvent: true,
bookingFields: true,
}, },
}, },
responses: true,
uid: true, uid: true,
destinationCalendar: true, destinationCalendar: true,
}, },
@@ -96,6 +99,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
title: booking.title, title: booking.title,
description: booking.description || undefined, description: booking.description || undefined,
customInputs: isPrismaObjOrUndefined(booking.customInputs), customInputs: isPrismaObjOrUndefined(booking.customInputs),
...getCalEventResponses({
bookingFields: booking.eventType?.bookingFields ?? null,
booking,
}),
location: booking.location ?? "", location: booking.location ?? "",
startTime: booking.startTime.toISOString(), startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(), endTime: booking.endTime.toISOString(),
+37 -4
View File
@@ -1,4 +1,12 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments";
import prisma from "@calcom/prisma";
const querySchema = z.object({
page: z.coerce.number().min(0).optional().default(0),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey; const apiKey = req.headers.authorization || req.query.apiKey;
@@ -11,10 +19,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return; return;
} }
/** const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
* TODO: const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
* Remove this endpoint let { page: pageNumber } = querySchema.parse(req.query);
*/
while (true) {
const teams = await prisma.team.findMany({
where: {
slug: {
not: null,
},
},
select: {
id: true,
},
skip: pageNumber * pageSize,
take: pageSize,
});
if (teams.length === 0) {
break;
}
for (const team of teams) {
await updateQuantitySubscriptionFromStripe(team.id);
await delay(100); // Adjust the delay as needed to avoid rate limiting
}
pageNumber++;
}
res.json({ ok: true }); res.json({ ok: true });
} }
+3 -3
View File
@@ -88,7 +88,7 @@ export default async function handler(req: NextApiRequest) {
ogConfig ogConfig
) as { body: Buffer }; ) as { body: Buffer };
return new Response(img.body, { status: 200 }); return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
} }
case "app": { case "app": {
const { name, description, slug } = appSchema.parse({ const { name, description, slug } = appSchema.parse({
@@ -101,7 +101,7 @@ export default async function handler(req: NextApiRequest) {
body: Buffer; body: Buffer;
}; };
return new Response(img.body, { status: 200 }); return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
} }
case "generic": { case "generic": {
@@ -115,7 +115,7 @@ export default async function handler(req: NextApiRequest) {
body: Buffer; body: Buffer;
}; };
return new Response(img.body, { status: 200 }); return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
} }
default: default:
+3 -3
View File
@@ -14,7 +14,7 @@ import { FiSearch } from "@calcom/ui/components/icon";
import AppsLayout from "@components/apps/layouts/AppsLayout"; import AppsLayout from "@components/apps/layouts/AppsLayout";
import { ssgInit } from "@server/lib/ssg"; import { ssrInit } from "@server/lib/ssr";
const tabs: HorizontalTabItemProps[] = [ const tabs: HorizontalTabItemProps[] = [
{ {
@@ -88,7 +88,7 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
export const getServerSideProps = async (context: GetServerSidePropsContext) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context; const { req, res } = context;
const ssg = await ssgInit(context); const ssr = await ssrInit(context);
const session = await getServerSession({ req, res }); const session = await getServerSession({ req, res });
@@ -119,7 +119,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return b.count - a.count; return b.count - a.count;
}), }),
appStore, appStore,
trpcState: ssg.dehydrate(), trpcState: ssr.dehydrate(),
}, },
}; };
}; };
+14 -135
View File
@@ -6,7 +6,7 @@ import type { GetServerSidePropsContext } from "next";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { RRule } from "rrule"; import { RRule } from "rrule";
import { z } from "zod"; import { z } from "zod";
@@ -36,6 +36,7 @@ import {
formatToLocalizedTimezone, formatToLocalizedTimezone,
} from "@calcom/lib/date-fns"; } from "@calcom/lib/date-fns";
import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { getBookingWithResponses } from "@calcom/lib/getBooking";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
@@ -51,7 +52,6 @@ import { Button, EmailInput, HeadSeo, Badge } from "@calcom/ui";
import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon"; import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock"; import { timeZone } from "@lib/clock";
import { getBookingWithResponses } from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import CancelBooking from "@components/booking/CancelBooking"; import CancelBooking from "@components/booking/CancelBooking";
@@ -59,114 +59,6 @@ import EventReservationSchema from "@components/schemas/EventReservationSchema";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
function redirectToExternalUrl(url: string) {
window.parent.location.href = url;
}
/**
* Redirects to external URL with query params from current URL.
* Query Params and Hash Fragment if present in external URL are kept intact.
*/
function RedirectionToast({ url }: { url: string }) {
const [timeRemaining, setTimeRemaining] = useState(10);
const [isToastVisible, setIsToastVisible] = useState(true);
const { t } = useLocale();
const timerRef = useRef<number | null>(null);
const router = useRouter();
const { cancel: isCancellationMode } = querySchema.parse(router.query);
const urlWithSuccessParamsRef = useRef<string | null>();
if (isCancellationMode && timerRef.current) {
setIsToastVisible(false);
}
useEffect(() => {
if (!isToastVisible && timerRef.current) {
window.clearInterval(timerRef.current);
}
}, [isToastVisible]);
useEffect(() => {
const parsedExternalUrl = new URL(url);
const parsedSuccessUrl = new URL(document.URL);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */ //https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type
for (const [name, value] of parsedExternalUrl.searchParams.entries()) {
parsedSuccessUrl.searchParams.set(name, value);
}
const urlWithSuccessParams =
parsedExternalUrl.origin +
parsedExternalUrl.pathname +
"?" +
parsedSuccessUrl.searchParams.toString() +
parsedExternalUrl.hash;
urlWithSuccessParamsRef.current = urlWithSuccessParams;
timerRef.current = window.setInterval(() => {
if (timeRemaining > 0) {
setTimeRemaining((timeRemaining) => {
return timeRemaining - 1;
});
} else {
redirectToExternalUrl(urlWithSuccessParams);
window.clearInterval(timerRef.current as number);
}
}, 1000);
return () => {
window.clearInterval(timerRef.current as number);
};
}, [timeRemaining, url]);
if (!isToastVisible) {
return null;
}
return (
<>
<div className="relative z-[60] pb-2 sm:pb-5">
<div className="mx-auto w-full sm:max-w-7xl sm:px-2 lg:px-8">
<div className="border border-green-600 bg-green-500 p-2 sm:p-3">
<div className="flex flex-wrap items-center justify-between">
<div className="flex w-0 flex-1 items-center">
<p className="truncate font-medium text-white sm:mx-3">
<span className="md:hidden">Redirecting to {url} ...</span>
<span className="hidden md:inline">
{t("you_are_being_redirected", { url, seconds: timeRemaining })}
</span>
</p>
</div>
<div className="order-3 mt-2 w-full flex-shrink-0 sm:order-2 sm:mt-0 sm:w-auto">
<button
onClick={() => {
if (urlWithSuccessParamsRef.current) {
redirectToExternalUrl(urlWithSuccessParamsRef.current);
}
}}
className="flex w-full items-center justify-center rounded-sm border border-transparent bg-white px-4 py-2 text-sm font-medium text-green-600 hover:bg-green-50">
{t("continue")}
</button>
</div>
<div className="order-2 flex-shrink-0 sm:order-3 sm:ml-2">
<button
type="button"
onClick={() => {
setIsToastVisible(false);
}}
className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white">
<FiX className="h-6 w-6 text-white" />
</button>
</div>
</div>
</div>
</div>
</div>
</>
);
}
type SuccessProps = inferSSRProps<typeof getServerSideProps>; type SuccessProps = inferSSRProps<typeof getServerSideProps>;
const stringToBoolean = z const stringToBoolean = z
@@ -176,13 +68,14 @@ const stringToBoolean = z
const querySchema = z.object({ const querySchema = z.object({
uid: z.string(), uid: z.string(),
allRemainingBookings: stringToBoolean, email: z.string().optional(),
eventTypeSlug: z.string().optional(),
cancel: stringToBoolean, cancel: stringToBoolean,
allRemainingBookings: stringToBoolean,
changes: stringToBoolean, changes: stringToBoolean,
reschedule: stringToBoolean, reschedule: stringToBoolean,
isSuccessBookingPage: stringToBoolean, isSuccessBookingPage: stringToBoolean,
formerTime: z.string().optional(), formerTime: z.string().optional(),
email: z.string().optional(),
seatReferenceUid: 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} /> <CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}> <main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}> <div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
{isSuccessBookingPage && !isCancellationMode && eventType.successRedirectUrl ? (
<RedirectionToast url={eventType.successRedirectUrl} />
) : null}{" "}
<div <div
className={classNames( className={classNames(
shouldAlignCentrally ? "text-center" : "", shouldAlignCentrally ? "text-center" : "",
@@ -569,7 +459,8 @@ export default function Success(props: SuccessProps) {
</div> </div>
</> </>
)} )}
</div>
<div className="text-bookingdark dark:border-darkgray-200 mt-8 text-left dark:text-gray-300">
{Object.entries(bookingInfo.responses).map(([name, response]) => { {Object.entries(bookingInfo.responses).map(([name, response]) => {
const field = eventType.bookingFields.find((field) => field.name === name); const field = eventType.bookingFields.find((field) => field.name === name);
// We show location in the "where" section // We show location in the "where" section
@@ -584,15 +475,10 @@ export default function Success(props: SuccessProps) {
return ( return (
<> <>
<div className="mt-9 font-medium">{label}</div> <div className="mt-4 font-medium">{label}</div>
<div className="col-span-2 mb-2 mt-9"> <p className="break-words" data-testid="field-response" data-fob-field={field.name}>
<p {response.toString()}
className="break-words" </p>
data-testid="field-response"
data-fob-field={field.name}>
{response.toString()}
</p>
</div>
</> </>
); );
})} })}
@@ -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 = ( const handleSeatsEventTypeOnBooking = (
eventType: { eventType: {
seatsPerTimeSlot?: number | null; seatsPerTimeSlot?: number | null;
@@ -1021,9 +900,9 @@ const handleSeatsEventTypeOnBooking = (
export async function getServerSideProps(context: GetServerSidePropsContext) { export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context); const ssr = await ssrInit(context);
const parsedQuery = schema.safeParse(context.query); const parsedQuery = querySchema.safeParse(context.query);
if (!parsedQuery.success) return { notFound: true }; if (!parsedQuery.success) return { notFound: true };
const { uid, email, eventTypeSlug, cancel } = parsedQuery.data; const { uid, email, eventTypeSlug, cancel, isSuccessBookingPage } = parsedQuery.data;
const bookingInfoRaw = await prisma.booking.findFirst({ const bookingInfoRaw = await prisma.booking.findFirst({
where: { where: {
@@ -1120,7 +999,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs), customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
}; };
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl && !cancel) { if (eventType.successRedirectUrl && isSuccessBookingPage && !cancel) {
return { return {
redirect: { redirect: {
destination: eventType.successRedirectUrl, destination: eventType.successRedirectUrl,
+3 -1
View File
@@ -5,11 +5,12 @@ import type { LocationObject } from "@calcom/core/location";
import { privacyFilteredLocations } from "@calcom/core/location"; import { privacyFilteredLocations } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import { getWorkingHours } from "@calcom/lib/availability"; import { getWorkingHours } from "@calcom/lib/availability";
import type { GetBookingType } from "@calcom/lib/getBooking";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { availiblityPageEventTypeSelect } from "@calcom/prisma"; import { availiblityPageEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { GetBookingType } from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr"; import type { EmbedProps } from "@lib/withEmbedSsr";
@@ -119,6 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
hideBranding: u.hideBranding, hideBranding: u.hideBranding,
timeZone: u.timeZone, timeZone: u.timeZone,
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(hashedLink.eventType.description),
}); });
const [user] = users; const [user] = users;
+2
View File
@@ -1,6 +1,7 @@
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { bookEventTypeSelect } from "@calcom/prisma/selects"; import { bookEventTypeSelect } from "@calcom/prisma/selects";
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -93,6 +94,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
brandColor: u.brandColor, brandColor: u.brandColor,
darkBrandColor: u.darkBrandColor, darkBrandColor: u.darkBrandColor,
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}; };
})[0]; })[0];
+11 -25
View File
@@ -13,11 +13,9 @@ import type { EventLocationType } from "@calcom/core/location";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { validateIntervalLimitOrder } from "@calcom/lib"; import { validateIntervalLimitOrder } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import getEventTypeById from "@calcom/lib/getEventTypeById";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client"; import type { Prisma } from "@calcom/prisma/client";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -65,6 +63,7 @@ export type FormValues = {
hostPhoneNumber?: string; hostPhoneNumber?: string;
displayLocationPublicly?: boolean; displayLocationPublicly?: boolean;
phone?: string; phone?: string;
hostDefault?: string;
}[]; }[];
customInputs: CustomInputParsed[]; customInputs: CustomInputParsed[];
schedule: number | null; schedule: number | null;
@@ -365,9 +364,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
}; };
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => { const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
const { data, isLoading } = trpc.viewer.eventTypes.get.useQuery({ id: props.type }); const { data } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
if (isLoading || !data) return null;
return <EventTypePage {...data} />; return <EventTypePage {...(data as EventTypeSetupProps)} />;
}; };
export const getServerSideProps = async (context: GetServerSidePropsContext) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
@@ -393,26 +392,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}; };
} }
try { await ssr.viewer.eventTypes.get.prefetch({ id: typeParam });
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma }); return {
props: {
return { type: typeParam,
props: { trpcState: ssr.dehydrate(),
// session, },
type: typeParam, };
trpcState: ssr.dehydrate(),
initialData: {
eventType: res.eventType,
locationOptions: res.locationOptions,
team: res.team,
teamMembers: res.teamMembers,
currentUserMembership: res.currentUserMembership,
},
},
};
} catch (err) {
throw err;
}
}; };
export default EventTypePageWrapper; export default EventTypePageWrapper;
+1 -1
View File
@@ -141,7 +141,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
</div> </div>
<EventTypeDescription <EventTypeDescription
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon // @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
eventType={type} eventType={{ ...type, descriptionAsSafeHTML: type.safeDescription }}
shortenDescription shortenDescription
/> />
</Link> </Link>
+27 -15
View File
@@ -1,3 +1,4 @@
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { import {
AverageEventDurationChart, AverageEventDurationChart,
BookingKPICards, BookingKPICards,
@@ -7,7 +8,6 @@ import {
PopularEventsTable, PopularEventsTable,
} from "@calcom/features/insights/components"; } from "@calcom/features/insights/components";
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider"; import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
import { useFilterContext } from "@calcom/features/insights/context/provider";
import { Filters } from "@calcom/features/insights/filters"; import { Filters } from "@calcom/features/insights/filters";
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips"; import { UpgradeTip } from "@calcom/features/tips";
@@ -19,15 +19,11 @@ import { FiRefreshCcw, FiUserPlus, FiUsers } from "@calcom/ui/components/icon";
const Heading = () => { const Heading = () => {
const { t } = useLocale(); const { t } = useLocale();
const {
filter: { selectedTeamName },
} = useFilterContext();
return ( return (
<div className="min-w-52"> <div className="min-w-52 hidden md:block">
<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"> <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", { {t("analytics_for_organisation")}
organisationName: selectedTeamName,
})}
</h3> </h3>
<p className="hidden text-sm text-gray-500 md:block">{t("subtitle_analytics")}</p> <p className="hidden text-sm text-gray-500 md:block">{t("subtitle_analytics")}</p>
</div> </div>
@@ -39,17 +35,17 @@ export default function InsightsPage() {
const { data: user } = trpc.viewer.me.useQuery(); const { data: user } = trpc.viewer.me.useQuery();
const features = [ const features = [
{ {
icon: <FiUsers className="h-5 w-5 text-red-500" />, icon: <FiUsers className="h-5 w-5" />,
title: t("view_bookings_across"), title: t("view_bookings_across"),
description: t("view_bookings_across_description"), 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"), title: t("identify_booking_trends"),
description: t("identify_booking_trends_description"), 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"), title: t("spot_popular_event_types"),
description: t("spot_popular_event_types_description"), description: t("spot_popular_event_types_description"),
}, },
@@ -62,7 +58,7 @@ export default function InsightsPage() {
title={t("make_informed_decisions")} title={t("make_informed_decisions")}
description={t("make_informed_decisions_description")} description={t("make_informed_decisions_description")}
features={features} features={features}
background="/banners/insights.jpg" background="/tips/insights.jpg"
buttons={ buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2"> <div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup> <ButtonGroup>
@@ -79,7 +75,7 @@ export default function InsightsPage() {
<></> <></>
) : ( ) : (
<FiltersProvider> <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 /> <Heading />
<Filters /> <Filters />
</div> </div>
@@ -98,7 +94,7 @@ export default function InsightsPage() {
<LeastBookedTeamMembersTable /> <LeastBookedTeamMembersTable />
</div> </div>
<small className="block text-center text-gray-600"> <small className="block text-center text-gray-600">
{t("looking_for_more_analytics")} {t("looking_for_more_insights")}
<a <a
className="text-blue-500 hover:underline" 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..."> 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> </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 { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui"; import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText, Alert } from "@calcom/ui";
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal"; import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal"; import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
@@ -32,11 +32,14 @@ const TwoFactorAuthView = () => {
if (isLoading) return <SkeletonLoader />; if (isLoading) return <SkeletonLoader />;
const isCalProvider = user?.identityProvider === "CAL";
return ( return (
<> <>
<Meta title={t("2fa")} description={t("2fa_description")} /> <Meta title={t("2fa")} description={t("2fa_description")} />
{!isCalProvider && <Alert severity="neutral" message={t("2fa_disabled")} />}
<div className="mt-6 flex items-start space-x-4"> <div className="mt-6 flex items-start space-x-4">
<Switch <Switch
disabled={!isCalProvider}
checked={user?.twoFactorEnabled} checked={user?.twoFactorEnabled}
onCheckedChange={() => onCheckedChange={() =>
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true) user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
@@ -19,7 +19,7 @@ const OnboardTeamMembersPage = () => {
}; };
OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => ( OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => (
<WizardLayout currentStep={1} maxSteps={2}> <WizardLayout currentStep={2} maxSteps={2}>
{page} {page}
</WizardLayout> </WizardLayout>
); );
+9 -2
View File
@@ -3,7 +3,7 @@ import Head from "next/head";
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components"; import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getLayout } from "@components/layouts/WizardLayout"; import WizardLayout from "@components/layouts/WizardLayout";
const CreateNewTeamPage = () => { const CreateNewTeamPage = () => {
const { t } = useLocale(); const { t } = useLocale();
@@ -17,7 +17,14 @@ const CreateNewTeamPage = () => {
</> </>
); );
}; };
const LayoutWrapper = (page: React.ReactElement) => {
return (
<WizardLayout currentStep={1} maxSteps={2}>
{page}
</WizardLayout>
);
};
CreateNewTeamPage.getLayout = getLayout; CreateNewTeamPage.getLayout = LayoutWrapper;
export default CreateNewTeamPage; export default CreateNewTeamPage;
+10 -3
View File
@@ -10,7 +10,7 @@ import { CAL_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
import { md } from "@calcom/lib/markdownIt"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams"; import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
@@ -113,7 +113,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
<> <>
<div <div
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600" className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(team.bio || "") }} dangerouslySetInnerHTML={{ __html: team.safeBio }}
/> />
</> </>
)} )}
@@ -187,11 +187,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
...user, ...user,
avatar: CAL_URL + "/" + user.username + "/avatar.png", avatar: CAL_URL + "/" + user.username + "/avatar.png",
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(type.description),
})); }));
const safeBio = markdownToSafeHTML(team.bio) || "";
const members = team.members.map((member) => {
return { ...member, safeBio: markdownToSafeHTML(member.bio || "") };
});
return { return {
props: { props: {
team, team: { ...team, safeBio, members },
trpcState: ssr.dehydrate(), trpcState: ssr.dehydrate(),
}, },
} as const; } as const;
+4 -3
View File
@@ -4,12 +4,13 @@ import type { LocationObject } from "@calcom/core/location";
import { privacyFilteredLocations } from "@calcom/core/location"; import { privacyFilteredLocations } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import { getWorkingHours } from "@calcom/lib/availability"; import { getWorkingHours } from "@calcom/lib/availability";
import getBooking from "@calcom/lib/getBooking";
import type { GetBookingType } from "@calcom/lib/getBooking";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull } from "@lib/asStringOrNull"; import { asStringOrNull } from "@lib/asStringOrNull";
import type { GetBookingType } from "@lib/getBooking";
import getBooking from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr"; import type { EmbedProps } from "@lib/withEmbedSsr";
@@ -63,7 +64,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
select: { select: {
id: true, id: true,
name: true, name: true,
avatar: true,
username: true, username: true,
timeZone: true, timeZone: true,
hideBranding: true, hideBranding: true,
@@ -170,6 +170,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
hideBranding, hideBranding,
timeZone, timeZone,
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}); });
eventTypeObject.availability = []; 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 { privacyFilteredLocations } from "@calcom/app-store/locations";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import type { GetBookingType } from "@calcom/lib/getBooking";
import getBooking from "@calcom/lib/getBooking";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull"; import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import type { GetBookingType } from "@lib/getBooking";
import getBooking from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage"; import BookingPage from "@components/booking/pages/BookingPage";
@@ -121,6 +122,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
image: u.avatar, image: u.avatar,
slug: u.username, slug: u.username,
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
}; };
})[0]; })[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 ({ 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", { api("on", {
action: "*", action: "*",
callback: (e: any) => { callback: (e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work
+10 -4
View File
@@ -97,13 +97,19 @@ test.describe("Reschedule Tests", async () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible(); await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking.uid } }); const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: booking.uid } });
const rescheduledBooking = await prisma.booking.findFirst({ where: { uid: booking.uid } }); const rescheduledBooking = await prisma.booking.findFirstOrThrow({ where: { uid: booking.uid } });
expect(newBooking).not.toBeNull(); 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 }) => { 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 Disallow: /static/locales
# Specifically allow access to OG Image api, otherwise eg Twitter won't render these images. # Specifically allow access to OG Image api, otherwise eg Twitter won't render these images.
Allow: /api/social/og/image Allow: /api/social/og/image
# Specifically allow access to api/app-store to allow search engines to fetch assets
Allow: /api/app-store/
+14 -6
View File
@@ -404,6 +404,7 @@
"add_time_availability": "Add new time slot", "add_time_availability": "Add new time slot",
"add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.", "add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.",
"2fa": "Two-Factor Authentication", "2fa": "Two-Factor Authentication",
"2fa_disabled": "Two-Factor authentication can only be enabled for email and password authentication",
"enable_2fa": "Enable two-factor authentication", "enable_2fa": "Enable two-factor authentication",
"disable_2fa": "Disable two-factor authentication", "disable_2fa": "Disable two-factor authentication",
"disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.", "disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.",
@@ -1218,6 +1219,7 @@
"impersonation": "Impersonation", "impersonation": "Impersonation",
"impersonation_description": "Settings to manage user impersonation", "impersonation_description": "Settings to manage user impersonation",
"users": "Users", "users": "Users",
"user": "User",
"profile_description": "Manage settings for your {{appName}} profile", "profile_description": "Manage settings for your {{appName}} profile",
"users_description": "Here you can find a list of all users", "users_description": "Here you can find a list of all users",
"users_listing": "User listing", "users_listing": "User listing",
@@ -1499,7 +1501,7 @@
"date_overrides_update_btn": "Update Override", "date_overrides_update_btn": "Update Override",
"event_type_duplicate_copy_text": "{{slug}}-copy", "event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "Set as default", "set_as_default": "Set as default",
"hide_eventtype_details": "Hide EventType Details", "hide_eventtype_details": "Hide event type details",
"show_navigation": "Show navigation", "show_navigation": "Show navigation",
"hide_navigation": "Hide navigation", "hide_navigation": "Hide navigation",
"verification_code_sent": "Verification code sent", "verification_code_sent": "Verification code sent",
@@ -1513,7 +1515,7 @@
"create_your_first_team_webhook_description": "Create your first webhook for this team event type", "create_your_first_team_webhook_description": "Create your first webhook for this team event type",
"create_webhook_team_event_type": "Create a webhook for this team event type", "create_webhook_team_event_type": "Create a webhook for this team event type",
"disable_success_page": "Disable Success Page (only works if you have a redirect URL)", "disable_success_page": "Disable Success Page (only works if you have a redirect URL)",
"invalid_admin_password": "You are set as an admin but you do not have a password length of at least 15 characters", "invalid_admin_password": "You are admin but you do not have a password length of at least 15 characters or no 2FA yet",
"change_password_admin": "Change Password to gain admin access", "change_password_admin": "Change Password to gain admin access",
"username_already_taken": "Username is already taken", "username_already_taken": "Username is already taken",
"assignment": "Assignment", "assignment": "Assignment",
@@ -1608,6 +1610,7 @@
"default_app_link_title": "Set a default app link", "default_app_link_title": "Set a default app link",
"default_app_link_description": "Setting a default app link allows all newly created event types to use the app link you set.", "default_app_link_description": "Setting a default app link allows all newly created event types to use the app link you set.",
"change_default_conferencing_app": "Set as default", "change_default_conferencing_app": "Set as default",
"organizer_default_conferencing_app": "Organizer's default app",
"under_maintenance": "Down for maintenance", "under_maintenance": "Down for maintenance",
"under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.", "under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.",
"event_type_seats": "{{numberOfSeats}} seats", "event_type_seats": "{{numberOfSeats}} seats",
@@ -1670,7 +1673,7 @@
"default_conferencing_bulk_title": "Bulk update existing event types", "default_conferencing_bulk_title": "Bulk update existing event types",
"requires_at_least_one_schedule": "You are required to have at least one schedule", "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", "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", "add_filter": "Add filter",
"select_user": "Select User", "select_user": "Select User",
"select_event_type": "Select Event Type", "select_event_type": "Select Event Type",
@@ -1686,7 +1689,12 @@
"events_rescheduled": "Events Rescheduled", "events_rescheduled": "Events Rescheduled",
"from_last_period": "from last period", "from_last_period": "from last period",
"from_to_date_period": "From: {{startDate}} To: {{endDate}}", "from_to_date_period": "From: {{startDate}} To: {{endDate}}",
"analytics_for_organisation": "Analytics for {{organisationName}}", "analytics_for_organisation": "Insights",
"subtitle_analytics": "This is a organisation analytics", "subtitle_analytics": "Learn more about your team's activity",
"event_trends": "Event Trends" "redirect_url_warning": "Adding a redirect will disable the success page. Make sure to mention \"Booking Confirmed\" on your custom success page.",
"event_trends": "Event Trends",
"clear_filters": "Clear Filters",
"insights": "Insights",
"insights_no_data_found_for_filter": "No data found for the selected filter or selected dates.",
"email_invite_team": "{{email}} has been invited"
} }
@@ -1213,6 +1213,7 @@
"impersonation": "Suplantación", "impersonation": "Suplantación",
"impersonation_description": "Configuración para administrar la suplantación del usuario", "impersonation_description": "Configuración para administrar la suplantación del usuario",
"users": "Usuarios", "users": "Usuarios",
"user": "Usuario",
"profile_description": "Administra los ajustes para tu perfil de {{appName}}", "profile_description": "Administra los ajustes para tu perfil de {{appName}}",
"users_description": "Aquí puede encontrar una lista de todos los usuarios", "users_description": "Aquí puede encontrar una lista de todos los usuarios",
"users_listing": "Lista de usuarios", "users_listing": "Lista de usuarios",
File renamed without changes.
File renamed without changes.
+2 -2
View File
@@ -148,8 +148,6 @@ select:focus {
--tw-ring-color: var(--brand-color); --tw-ring-color: var(--brand-color);
border-color: var(--brand-color); border-color: var(--brand-color);
} }
/* https://stackoverflow.com/a/69308190 */
input[type='text']:focus { box-shadow: none; }
@layer components { @layer components {
@@ -497,3 +495,5 @@ hr {
::-webkit-search-cancel-button { ::-webkit-search-cancel-button {
-webkit-appearance: none; -webkit-appearance: none;
} }
+3 -1
View File
@@ -70,6 +70,7 @@
"test-playwright": "yarn playwright test --config=playwright.config.ts", "test-playwright": "yarn playwright test --config=playwright.config.ts",
"test": "jest", "test": "jest",
"type-check": "turbo run type-check", "type-check": "turbo run type-check",
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
"web": "yarn workspace @calcom/web" "web": "yarn workspace @calcom/web"
}, },
"devDependencies": { "devDependencies": {
@@ -86,8 +87,9 @@
"jest-watch-select-projects": "^2.0.0", "jest-watch-select-projects": "^2.0.0",
"jest-watch-typeahead": "^2.0.0", "jest-watch-typeahead": "^2.0.0",
"lint-staged": "^12.5.0", "lint-staged": "^12.5.0",
"prettier": "^2.8.4", "prettier": "^2.8.6",
"ts-jest": "^28.0.8", "ts-jest": "^28.0.8",
"tsc-absolute": "^1.0.0",
"typescript": "^4.9.4" "typescript": "^4.9.4"
}, },
"dependencies": { "dependencies": {
+3 -1
View File
@@ -1,7 +1,7 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json"; import type seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json";
import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants"; import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants";
import execSync from "./utils/execSync"; import execSync from "./utils/execSync";
@@ -127,6 +127,8 @@ export const BaseAppFork = {
.replace(/_DESCRIPTION_/g, description) .replace(/_DESCRIPTION_/g, description)
.replace(/_APP_DIR_/g, slug) .replace(/_APP_DIR_/g, slug)
); );
// New monorepo package has been added, so we need to run yarn again
await execSync("yarn");
}, },
delete: async function ({ slug, isTemplate }: { slug: string; isTemplate: boolean }) { delete: async function ({ slug, isTemplate }: { slug: string; isTemplate: boolean }) {
@@ -40,6 +40,7 @@ export const EventTypeAddonMap = {
fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")), fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")),
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")), ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")), giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
"google-tag-manager": dynamic(() => import("./google-tag-manager/components/EventTypeAppCardInterface")),
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")), plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")), qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")),
rainbow: dynamic(() => import("./rainbow/components/EventTypeAppCardInterface")), rainbow: dynamic(() => import("./rainbow/components/EventTypeAppCardInterface")),
@@ -6,6 +6,7 @@ import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
import { appKeysSchema as fathom_zod_ts } from "./fathom/zod"; import { appKeysSchema as fathom_zod_ts } from "./fathom/zod";
import { appKeysSchema as ga4_zod_ts } from "./ga4/zod"; import { appKeysSchema as ga4_zod_ts } from "./ga4/zod";
import { appKeysSchema as giphy_zod_ts } from "./giphy/zod"; import { appKeysSchema as giphy_zod_ts } from "./giphy/zod";
import { appKeysSchema as google_tag_manager_zod_ts } from "./google-tag-manager/zod";
import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod"; import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod"; import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod";
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
@@ -30,6 +31,7 @@ export const appKeysSchemas = {
fathom: fathom_zod_ts, fathom: fathom_zod_ts,
ga4: ga4_zod_ts, ga4: ga4_zod_ts,
giphy: giphy_zod_ts, giphy: giphy_zod_ts,
"google-tag-manager": google_tag_manager_zod_ts,
googlecalendar: googlecalendar_zod_ts, googlecalendar: googlecalendar_zod_ts,
hubspot: hubspot_zod_ts, hubspot: hubspot_zod_ts,
larkcalendar: larkcalendar_zod_ts, larkcalendar: larkcalendar_zod_ts,
@@ -17,6 +17,7 @@ import facetime_config_json from "./facetime/config.json";
import fathom_config_json from "./fathom/config.json"; import fathom_config_json from "./fathom/config.json";
import ga4_config_json from "./ga4/config.json"; import ga4_config_json from "./ga4/config.json";
import { metadata as giphy__metadata_ts } from "./giphy/_metadata"; import { metadata as giphy__metadata_ts } from "./giphy/_metadata";
import google_tag_manager_config_json from "./google-tag-manager/config.json";
import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata"; import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata";
import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata"; import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata";
import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata"; import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata";
@@ -39,6 +40,7 @@ import sendgrid_config_json from "./sendgrid/config.json";
import signal_config_json from "./signal/config.json"; import signal_config_json from "./signal/config.json";
import sirius_video_config_json from "./sirius_video/config.json"; import sirius_video_config_json from "./sirius_video/config.json";
import { metadata as stripepayment__metadata_ts } from "./stripepayment/_metadata"; import { metadata as stripepayment__metadata_ts } from "./stripepayment/_metadata";
import sylapsvideo_config_json from "./sylapsvideo/config.json";
import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata"; import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata";
import telegram_config_json from "./telegram/config.json"; import telegram_config_json from "./telegram/config.json";
import basic_config_json from "./templates/basic/config.json"; import basic_config_json from "./templates/basic/config.json";
@@ -75,6 +77,7 @@ export const appStoreMetadata = {
fathom: fathom_config_json, fathom: fathom_config_json,
ga4: ga4_config_json, ga4: ga4_config_json,
giphy: giphy__metadata_ts, giphy: giphy__metadata_ts,
"google-tag-manager": google_tag_manager_config_json,
googlecalendar: googlecalendar__metadata_ts, googlecalendar: googlecalendar__metadata_ts,
googlevideo: googlevideo__metadata_ts, googlevideo: googlevideo__metadata_ts,
hubspot: hubspot__metadata_ts, hubspot: hubspot__metadata_ts,
@@ -97,6 +100,7 @@ export const appStoreMetadata = {
signal: signal_config_json, signal: signal_config_json,
sirius_video: sirius_video_config_json, sirius_video: sirius_video_config_json,
stripepayment: stripepayment__metadata_ts, stripepayment: stripepayment__metadata_ts,
sylapsvideo: sylapsvideo_config_json,
tandemvideo: tandemvideo__metadata_ts, tandemvideo: tandemvideo__metadata_ts,
telegram: telegram_config_json, telegram: telegram_config_json,
basic: basic_config_json, basic: basic_config_json,
@@ -6,6 +6,7 @@ import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
import { appDataSchema as fathom_zod_ts } from "./fathom/zod"; import { appDataSchema as fathom_zod_ts } from "./fathom/zod";
import { appDataSchema as ga4_zod_ts } from "./ga4/zod"; import { appDataSchema as ga4_zod_ts } from "./ga4/zod";
import { appDataSchema as giphy_zod_ts } from "./giphy/zod"; import { appDataSchema as giphy_zod_ts } from "./giphy/zod";
import { appDataSchema as google_tag_manager_zod_ts } from "./google-tag-manager/zod";
import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod"; import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod"; import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod";
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
@@ -30,6 +31,7 @@ export const appDataSchemas = {
fathom: fathom_zod_ts, fathom: fathom_zod_ts,
ga4: ga4_zod_ts, ga4: ga4_zod_ts,
giphy: giphy_zod_ts, giphy: giphy_zod_ts,
"google-tag-manager": google_tag_manager_zod_ts,
googlecalendar: googlecalendar_zod_ts, googlecalendar: googlecalendar_zod_ts,
hubspot: hubspot_zod_ts, hubspot: hubspot_zod_ts,
larkcalendar: larkcalendar_zod_ts, larkcalendar: larkcalendar_zod_ts,
@@ -17,6 +17,7 @@ export const apiHandlers = {
fathom: import("./fathom/api"), fathom: import("./fathom/api"),
ga4: import("./ga4/api"), ga4: import("./ga4/api"),
giphy: import("./giphy/api"), giphy: import("./giphy/api"),
"google-tag-manager": import("./google-tag-manager/api"),
googlecalendar: import("./googlecalendar/api"), googlecalendar: import("./googlecalendar/api"),
googlevideo: import("./googlevideo/api"), googlevideo: import("./googlevideo/api"),
hubspot: import("./hubspot/api"), hubspot: import("./hubspot/api"),
@@ -39,6 +40,7 @@ export const apiHandlers = {
signal: import("./signal/api"), signal: import("./signal/api"),
sirius_video: import("./sirius_video/api"), sirius_video: import("./sirius_video/api"),
stripepayment: import("./stripepayment/api"), stripepayment: import("./stripepayment/api"),
sylapsvideo: import("./sylapsvideo/api"),
tandemvideo: import("./tandemvideo/api"), tandemvideo: import("./tandemvideo/api"),
telegram: import("./telegram/api"), telegram: import("./telegram/api"),
basic: import("./templates/basic/api"), basic: import("./templates/basic/api"),
+1
View File
@@ -10,6 +10,7 @@ items:
{DESCRIPTION} {DESCRIPTION}
### Cron's most valuable features ### Cron's most valuable features
<!-- From "FAQs: What are Cron’s most valuable features?" <!-- From "FAQs: What are Cron’s most valuable features?"
https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 --> https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 -->
@@ -0,0 +1,16 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
createCredential: ({ appType, user, slug }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
};
export default handler;
@@ -0,0 +1 @@
export { default as add } from "./add";
@@ -0,0 +1,38 @@
import { useState } from "react";
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import AppCard from "@calcom/app-store/_components/AppCard";
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
import { TextField } from "@calcom/ui";
import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
const trackingId = getAppData("trackingId");
const [enabled, setEnabled] = useState(getAppData("enabled"));
return (
<AppCard
setAppData={setAppData}
app={app}
switchOnClick={(e) => {
if (!e) {
setEnabled(false);
} else {
setEnabled(true);
}
}}
switchChecked={enabled}>
<TextField
name="Tracking ID"
value={trackingId}
onChange={(e) => {
setAppData("trackingId", e.target.value);
}}
/>
</AppCard>
);
};
export default EventTypeAppCard;
@@ -0,0 +1,28 @@
{
"name": "Google Tag Manager",
"slug": "google-tag-manager",
"type": "google-tag-manager_analytics",
"imageSrc": "icon.svg",
"logo": "icon.svg",
"url": "https://tagmanager.google.com",
"variant": "analytics",
"categories": [
"analytics"
],
"publisher": "Black Lemon",
"email": "support@blacklemon.dk",
"description": "App to install Google Tag Manager",
"extendsFeature": "EventType",
"appData": {
"tag": {
"scripts": [
{
"content":"(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-{TRACKING_ID}');"
}
]
}
},
"isTemplate": false,
"__createdUsingCli": true,
"__template": "booking-pages-tag"
}
@@ -0,0 +1 @@
export * as api from "./api";
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/google-tag-manager",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "App to install Google Tag Manager"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

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

After

Width:  |  Height:  |  Size: 695 B

@@ -0,0 +1,11 @@
import { z } from "zod";
import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod";
export const appDataSchema = eventTypeAppCardZod.merge(
z.object({
trackingId: z.string(),
})
);
export const appKeysSchema = z.object({});
@@ -165,6 +165,7 @@ export default class GoogleCalendarService implements Calendar {
type: "google_calendar", type: "google_calendar",
password: "", password: "",
url: "", url: "",
iCalUID: event.data.iCalUID,
}); });
} }
); );
@@ -201,6 +202,9 @@ export default class GoogleCalendarService implements Calendar {
id: String(event.organizer.id), id: String(event.organizer.id),
organizer: true, organizer: true,
responseStatus: "accepted", responseStatus: "accepted",
email: event.destinationCalendar?.externalId
? event.destinationCalendar.externalId
: event.organizer.email,
}, },
// eslint-disable-next-line // eslint-disable-next-line
...eventAttendees, ...eventAttendees,
@@ -268,6 +272,7 @@ export default class GoogleCalendarService implements Calendar {
type: "google_calendar", type: "google_calendar",
password: "", password: "",
url: "", url: "",
iCalUID: evt.data.iCalUID,
}); });
} }
return resolve(evt?.data); return resolve(evt?.data);
+2
View File
@@ -20,6 +20,7 @@ import * as plausible from "./plausible";
import * as salesforce from "./salesforce"; import * as salesforce from "./salesforce";
import * as sendgrid from "./sendgrid"; import * as sendgrid from "./sendgrid";
import * as stripepayment from "./stripepayment"; import * as stripepayment from "./stripepayment";
import * as sylapsvideo from "./sylapsvideo";
import * as tandemvideo from "./tandemvideo"; import * as tandemvideo from "./tandemvideo";
import * as vital from "./vital"; import * as vital from "./vital";
import * as wipemycalother from "./wipemycalother"; import * as wipemycalother from "./wipemycalother";
@@ -38,6 +39,7 @@ const appStore = {
hubspot, hubspot,
huddle01video, huddle01video,
jitsivideo, jitsivideo,
sylapsvideo,
larkcalendar, larkcalendar,
office365calendar, office365calendar,
office365video, office365video,
+25 -4
View File
@@ -12,13 +12,20 @@ export type DefaultEventLocationType = {
type: DefaultEventLocationTypeEnum; type: DefaultEventLocationTypeEnum;
label: string; label: string;
messageForOrganizer: string; messageForOrganizer: string;
category: "in person" | "other" | "phone"; category: "in person" | "conferencing" | "other" | "phone";
iconUrl: string; iconUrl: string;
urlRegExp?: string; urlRegExp?: string;
// HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places. // HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places.
variable: "locationType" | "locationAddress" | "address" | "locationLink" | "locationPhoneNumber" | "phone"; variable:
defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "phone"; | "locationType"
| "locationAddress"
| "address"
| "locationLink"
| "locationPhoneNumber"
| "phone"
| "hostDefault";
defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone";
} & ( } & (
| { | {
organizerInputType: "phone" | "text" | null; organizerInputType: "phone" | "text" | null;
@@ -60,6 +67,7 @@ export enum DefaultEventLocationTypeEnum {
*/ */
UserPhone = "userPhone", UserPhone = "userPhone",
Link = "link", Link = "link",
Conferencing = "conferencing",
} }
export const defaultLocations: DefaultEventLocationType[] = [ export const defaultLocations: DefaultEventLocationType[] = [
@@ -88,6 +96,17 @@ export const defaultLocations: DefaultEventLocationType[] = [
iconUrl: "/map-pin.svg", iconUrl: "/map-pin.svg",
category: "in person", category: "in person",
}, },
{
default: true,
type: DefaultEventLocationTypeEnum.Conferencing,
iconUrl: "/link.svg",
organizerInputType: null,
label: "organizer_default_conferencing_app",
variable: "hostDefault",
defaultValueVariable: "hostDefault",
category: "conferencing",
messageForOrganizer: "",
},
{ {
default: true, default: true,
type: DefaultEventLocationTypeEnum.Link, type: DefaultEventLocationTypeEnum.Link,
@@ -130,7 +149,9 @@ export const defaultLocations: DefaultEventLocationType[] = [
export type LocationObject = { export type LocationObject = {
type: string; type: string;
displayLocationPublicly?: boolean; displayLocationPublicly?: boolean;
} & Partial<Record<"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "phone", string>>; } & Partial<
Record<"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone", string>
>;
// integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video // integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video
export type BookingLocationValue = string; export type BookingLocationValue = string;
@@ -73,7 +73,9 @@ export default class Office365CalendarService implements Calendar {
body: JSON.stringify(this.translateEvent(event)), body: JSON.stringify(this.translateEvent(event)),
}); });
return handleErrorsJson(response); const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
return { ...responseJson, iCalUID: responseJson.iCalUId };
} catch (error) { } catch (error) {
this.log.error(error); this.log.error(error);
@@ -88,7 +90,9 @@ export default class Office365CalendarService implements Calendar {
body: JSON.stringify(this.translateEvent(event)), body: JSON.stringify(this.translateEvent(event)),
}); });
return handleErrorsRaw(response); const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
return { ...responseJson, iCalUID: responseJson.iCalUId };
} catch (error) { } catch (error) {
this.log.error(error); this.log.error(error);
+1
View File
@@ -8,6 +8,7 @@
], ],
"scripts": { "scripts": {
"type-check": "tsc --pretty --noEmit", "type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit",
"lint": "eslint . --ext .ts,.js,.tsx,.jsx", "lint": "eslint . --ext .ts,.js,.tsx,.jsx",
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix", "lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json" "lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json"
@@ -1,9 +1,12 @@
import type { ChangeEvent } from "react";
import type { Settings, Widgets, SelectWidgetProps } from "react-awesome-query-builder"; import type { Settings, Widgets, SelectWidgetProps } from "react-awesome-query-builder";
// Figure out why routing-forms/env.d.ts doesn't work // Figure out why routing-forms/env.d.ts doesn't work
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
import BasicConfig from "react-awesome-query-builder/lib/config/basic"; import BasicConfig from "react-awesome-query-builder/lib/config/basic";
import { EmailField } from "@calcom/ui";
import widgetsComponents from "../widgets"; import widgetsComponents from "../widgets";
const { const {
@@ -92,8 +95,18 @@ const widgets: Widgets & { [key in keyof Widgets]: Widgets[key] & { type: string
if (!props) { if (!props) {
return <div />; return <div />;
} }
// TODO: Use EmailField component for Routing Form Email field
return <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")} title={t("teams_plan_required")}
description={t("routing_forms_are_a_great_way")} description={t("routing_forms_are_a_great_way")}
features={features} features={features}
background="/banners/routing-forms.jpg" background="/tips/routing-forms.jpg"
isParentLoading={isLoading && <SkeletonLoaderTeamList />} isParentLoading={isLoading && <SkeletonLoaderTeamList />}
buttons={ buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2"> <div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
@@ -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

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