Compare commits

...
359 Commits
Author SHA1 Message Date
Peer Richelsen 7ce6454915 Update package.json
release.yml / release (push) Canceled after 0s
2023-04-18 14:09:25 +02:00
Crowdin Bot 93cb21f55a New Crowdin translations by Github Action 2023-04-18 10:09:05 +00:00
0aca769d06 Allow editing workflow templates (#8028)
* add event end time as variable

* add timezone as new variable

* add first version of template prefill

* set template body when template is updated

* set reminder template body and subject when creating workflow

* set email subject when changes templates

* save emailBody and emailsubject for all templates + fix duplicate template text

* add more flexibility for templates

* remove console.log

* fix {ORAGANIZER} and {ATTENDEE} variable

* make sure to always send reminder body and not default template

* fix import

* remove email body text and match variables in templates

* handle translations of formatted variables

* fix email reminder template

* add cancel and reschedule link as variable

* add cancel and reschedule link for scheduled emails/sms

* make sure empty empty body and subject are set for reminder template

* add info message for testing workflow

* fix typo

* add sms template

* add migration to remove reminderBody and emailSubject

* add branding

* code clean up

* add hide branding everywhere

* fix sms reminder template

* set sms reminder template if sms body is empty

* fix custom inputs variables everywhere

* fix variable translations + other small fixes

* fix some type errors

* fix more type errors

* fix everything missing around cron job scheduling

* make sure to always use custom template for sms messages

* fix type error

* code clean up

* rename link to url

* Add debug logs

* Update handleNewBooking.ts

* Add debug logs

* removed unneded responses

* fix booking questions + UI improvements

* remove html email body when changing to sms action

* code clean up + comments

* code clean up

* code clean up

* remove comment

* more clear info message for timezone variable

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2023-04-18 11:08:09 +01:00
Nafees Nazik c0bda6257e Fix: dark mode issues (#8348)
* fix: dark mode

* fix: dark mode
2023-04-18 09:12:12 +00:00
Yagiz Nizipliandzomars ae22246f05 perf: improve application toggle flow (#8302)
* perf: improve application toggle flow

* Reverses logic for enabled apps

---------

Co-authored-by: zomars <zomars@me.com>
2023-04-18 01:17:16 -07:00
Crowdin Bot e2931d9fa2 New Crowdin translations by Github Action 2023-04-18 07:54:34 +00:00
Crowdin Bot e6a8a31b53 New Crowdin translations by Github Action 2023-04-18 07:53:41 +00:00
Crowdin Bot ee86c751f7 New Crowdin translations by Github Action 2023-04-18 07:52:53 +00:00
Crowdin Bot 073b212c05 New Crowdin translations by Github Action 2023-04-18 07:52:00 +00:00
Nafees NazikandBailey Pumfleet cd8e2124c4 fix: check box (#8328)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-04-18 08:51:03 +01:00
JoeandOmar López 9dd36bd20e fix: signup form submiting after api error (#8298)
Co-authored-by: Omar López <zomars@me.com>
2023-04-18 01:58:54 +00:00
Crowdin Bot b1404356ff New Crowdin translations by Github Action 2023-04-18 01:55:59 +00:00
Nafees Nazikandzomars f467bb7957 perf: use LRU cache for getServerSession (#8339)
* chore: add lru package

* feat: use LRU cache

* fix: type

* Updates package json

---------

Co-authored-by: zomars <zomars@me.com>
2023-04-18 01:55:01 +00:00
597b14cf4c perf: improve application list execution (#8301)
* perf: improve application list execution

* Removed unused interface

* Type fix

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-04-18 00:54:15 +00:00
2bdd6df755 avoid calendar cache on cold starts (#8311)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-04-17 23:56:08 +00:00
Yagiz NizipliandEfraín Rochín ad958f10bd perf: improve booking confirm authorization (#8304)
* perf: improve booking confirm authorization

* removes unreachable code

---------

Co-authored-by: Efraín Rochín <roae.85@gmail.com>
2023-04-17 23:50:34 +00:00
e5eb7c3906 perf: use getSlimServerSession for session retrieval (#8308)
* perf: use getSlimServerSession for session retrieval

Creates a new `getSlimServerSession` method and replaces all calls to `getServerSession`.

The new method is much faster than `getServerSession` with it not requiring the NextAuth options object which includes a number of additional packages.

Additionally introduces a primitive in-memory cache for slim sessions.

* fix: account for seconds in token.exp

* Reverts diffs

* Replaces getServerSession with slimmer version

* Update disable.ts

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-04-17 23:48:00 +00:00
zomars 667d42d5da Update yarn.lock 2023-04-17 15:46:43 -07:00
zomars a23a9ad15a Reverts PR checks limit 2023-04-17 15:26:17 -07:00
Crowdin Bot 4482ffc66b New Crowdin translations by Github Action 2023-04-17 22:23:34 +00:00
Udit Takkar 493efabd2e perf: remove sentry (#8334) 2023-04-17 22:22:44 +00:00
Crowdin Bot a0355e276d New Crowdin translations by Github Action 2023-04-17 19:55:05 +00:00
b5c993e153 perf: replace notIn with equals on booking (#8303)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-04-17 16:54:14 -03:00
Crowdin Bot da3ee3f1fe New Crowdin translations by Github Action 2023-04-17 16:20:34 +00:00
Udit TakkarandPeer Richelsen d9fc595444 fix: undo showRecordingsButton (#8262)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-17 16:19:24 +00:00
06d13e0f6e feat: Added option to select the exchange server version (#7908)
* Added option to select the exchange server version

* Fixed linting issues

* Removed yarn changes

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

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* Removed requested change

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-17 10:21:14 -04:00
Udit Takkar 7dc3266133 fix: use text-emphasis (#8332)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-04-17 14:54:39 +01:00
Hariom Balhara 2c96444058 Fix/Keep themes in peace across embed and booking pages and App (#8108) 2023-04-17 17:46:54 +05:30
Nafees Nazik 39c48e7f79 fix: some ui regressions (#8260)
* fix: icon size

* fix: icon size

* fix: border
2023-04-17 12:09:30 +00:00
Hariom Balhara b6e1989f17 Invalidate the form query on toggle to update toggle state in UI (#8325) 2023-04-17 17:34:25 +05:30
Nafees Nazik 45b5b2a376 fix: giphy input dark color 2023-04-17 17:27:30 +05:30
JoeandPeer Richelsen 05726ec94c fix: varients to layer (#8171)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-16 22:22:10 +00:00
Peer RichelsenandAlex van Andel 6340ff43d8 removed page_view tracker (#8267)
* removed page_view tracker

* Removed unused useTelemetry

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-16 21:58:47 +00:00
Peer Richelsen e64b26c7e1 fixed dark mode booker (#8315) 2023-04-16 21:47:07 +00:00
Joe c829a7de82 fix: remove line-clamp from plugins (#8309) 2023-04-16 23:05:44 +02:00
JoeandPeer Richelsen 0205bfb066 fix: MobileSettingsContainer height (#8295)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-16 20:18:56 +00:00
5fd7d63388 correction of position on 'Adding events to' button (#8290)
* correction to position

* Update apps/web/components/settings/CalendarSwitch.tsx

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-16 20:06:02 +00:00
e1ea2cb532 chore: UI username RTL (#8300)
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-16 19:35:17 +00:00
Joe 88d151aa35 fix: scroll-bar class for editor compnent (#8310) 2023-04-16 21:17:07 +02:00
Crowdin Bot 4e7c93a322 New Crowdin translations by Github Action 2023-04-16 18:50:25 +00:00
Peer Richelsen 281c7628c1 fixed bg 2023-04-16 20:49:12 +02:00
Crowdin Bot 1b24bc14cd New Crowdin translations by Github Action 2023-04-15 13:23:37 +00:00
Hariom Balhara be3d2fca05 Use name to generate unique for attribute for label (#8265) 2023-04-15 13:22:51 +00:00
Crowdin Bot 327e11e7c7 New Crowdin translations by Github Action 2023-04-15 11:51:04 +00:00
Peer Richelsen bd999345f4 Update package.json
release.yml / release (push) Canceled after 0s
2023-04-15 13:50:06 +02:00
Keith Williams 081776b4f9 Fixing syntax error in stale issue cron action (#8269) 2023-04-15 13:49:16 +02:00
Peer Richelsen 8c328ce5d7 fixed yarn dx (#8293) 2023-04-15 11:48:44 +00:00
Crowdin Bot 0fa2a6aea3 New Crowdin translations by Github Action
release.yml / release (push) Canceled after 0s
2023-04-15 09:21:02 +00:00
Peer Richelsen 440251f9c3 Update package.json 2023-04-15 11:20:06 +02:00
Omar LópezandLeo Giovanetti 7f6847f7c7 In-app users admin (#8035)
* WIP

* WIP

* Update trpc-router.ts

* Fixes

* Update trpc-router.ts

* Update zod-utils.ts

* Apply suggestions from code review

Co-authored-by: Leo Giovanetti <hello@leog.me>

* Feedback and dark mode fixes

* Feedback

* Fixes

* Update UserForm.tsx

---------

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-04-14 17:04:48 -07:00
zomars 9f1fa83a97 Update pr.yml 2023-04-14 17:04:35 -07:00
Joe Au-Yeung f00b112792 Change Stripe <CardElement /> to <PaymentElement /> (#8268)
* Use Stripe PaymentElement

* Only show needs confirmation, if the booking needs it

* Clean up

* Type fix

* More type fixes
2023-04-14 21:56:16 +00:00
Yagiz Nizipli 5339a489f5 perf: reduce array iteration on bookings router (#8276) 2023-04-14 21:29:43 +00:00
Yagiz Nizipli f7df204e0b perf: parallelize database calls to improve speed (#8275) 2023-04-14 21:27:10 +00:00
Alex van Andel 4975aba5e0 Move some dependencies into async (#8270)
* Move some dependencies into async

* Also remove jackson & jose from global scope
2023-04-14 14:25:15 -07:00
Omar López 29d61ad67b Update CODEOWNERS 2023-04-14 14:18:06 -07:00
Udit Takkar 6e555e64fa fix: spacing in booking item mobile (#8279)
release.yml / release (push) Canceled after 0s
2023-04-14 22:35:17 +02:00
Peer Richelsen c748bbc7e6 Update package.json 2023-04-14 22:34:32 +02:00
Leo Giovanetti 0681359bf3 Removing unneeded endpoint (#8280) 2023-04-14 20:27:37 +00:00
Yagiz Nizipli 4c9d4d7972 refactor: remove unnecessary or operation (#8274) 2023-04-14 11:03:24 -07:00
Crowdin Bot 67f8de42b3 New Crowdin translations by Github Action 2023-04-14 11:00:31 +00:00
Peer Richelsen 87584132b5 Update package.json
release.yml / release (push) Canceled after 0s
2023-04-14 12:59:31 +02:00
Crowdin Bot 5b943742e0 New Crowdin translations by Github Action 2023-04-14 08:05:36 +00:00
Hariom Balhara fe155b0b97 Fix: Sylaps app shown uninstalled after installation (#8256) 2023-04-14 10:04:41 +02:00
Udit TakkarandOmar López 6639588c2d fix: selectfield bug and min height (#8253)
* fix: selectfield bug and min height

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

* fix: remove overflow

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: Omar López <zomars@me.com>
2023-04-13 21:45:18 +00:00
zomars 7f9e8ca7a6 Runs pipelines only when a PR has a requested review 2023-04-13 14:24:26 -07:00
e478a46358 Feature: Reserve slots currently being booked (#6909)
* Reserving slot picked up on cache

* change memory-cache to database table to block slots while reservation completes

* remove memory-cache

* update realeaseAt field when same user change te selected Slot

* Change default time to book

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

* remove ip field and renews the session when the user remains in the booking form

* Remove duplicate router

* types fixes

* nit picks

* Update turbo.json

* Revert unrelated change

* Uses constant

* Constant already has a fallback

* Update slots.ts

* Unit test fixes

* slot reservation on user level and support seats

* types fixes and reserve slots on click

* Fix nit var name

---------

Co-authored-by: Efraín Rochín <roae.85@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-13 19:55:26 +00:00
Udit Takkar 7c9012738a feat: add isRecordingExist field and create api handler (#6777) 2023-04-13 19:07:10 +00:00
alannnc 5c763389f7 Fix workflows on seated events (#8034)
* Fix workflows on seated events

* Fix new args required in handleConfirmation

* Update function import to new name

* added email to override for workflows
2023-04-13 19:03:08 +00:00
Efraín Rochín 7fde30edf9 avoid get error when a user has no schedule defined in getUserAvailability function (#8170) 2023-04-13 18:26:45 +00:00
Hariom Balharaandsean-brydon 0e22e92cab Feat: Support setting custom css variables through embed API. (#8169)
* Add booker border specific custom variables

* Support configuring cssVars through embed API

* Update apps/web/pages/booking/[uid].tsx

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

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-04-13 18:26:31 +00:00
JoeandPeer Richelsen 17f48614ff fix: reccuring input width (#8227)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-13 18:26:13 +00:00
zomars 2f33b57eb7 Update yarn.lock 2023-04-13 11:12:25 -07:00
Nafees Nazik 6e5c3a88ee fix: clock icon size in booker page (#8249) 2023-04-13 20:08:09 +02:00
Alex van Andel 9024914f23 v2.7.16
release.yml / release (push) Canceled after 0s
2023-04-13 19:41:45 +02:00
sean-brydon 966bc08eb4 Fix radio item on future limits (#8247) 2023-04-13 11:58:08 +00:00
sean-brydon c545331cfe Few darkmode fixes - Avatar Tooltip | Forgot password page (#8242) 2023-04-13 10:59:32 +00:00
Peer Richelsen ce763f30f6 readded vibrancy for desktop app (#8245) 2023-04-13 10:59:09 +00:00
Peer Richelsen d453ae9f2b use brand for highlight (#8240) 2023-04-13 11:41:12 +01:00
Nafees Nazik 9f2b41e597 fix: Dark mode fixes (#8243)
* fix: event type card

* fix: text

* fix: border
2023-04-13 10:34:44 +01:00
GitStart-Cal.com 86e903ce4a show the link only if there's a username (#8238) 2023-04-13 07:04:52 +00:00
Crowdin Bot 9d3963436b New Crowdin translations by Github Action 2023-04-13 05:28:45 +00:00
alannnc d8218a6243 Fix payment app credential search (#8235) 2023-04-13 10:57:54 +05:30
Crowdin Bot cdc3833e09 New Crowdin translations by Github Action 2023-04-13 02:12:21 +00:00
Crowdin Bot 52b7e401a1 New Crowdin translations by Github Action 2023-04-13 02:11:27 +00:00
5170fc2424 Managed event-types (#6876)
* WIP

* Locked fields manager

* Leftovers

* Bad merge fix

* Type import fix

* Moving away from classes

* Progress refactoring locked logic

* Covering apps, webhooks and workflows

* Supporting webhooks and workflows (TBT)

* Restoring yarn.lock

* Progress

* Refactoring code, adding default values

* Fixing CRUD for children

* Connect app link and case-sensitive lib renaming

* Translation missing

* Locked indicators, empty screens, locations

* Member card and hidden status + missing i18n

* Missing existent children shown

* Showing preview for already created children

* Email notification almost in place

* Making progress over notif email

* Fixing nodemailer by mixed FE/BE mixup

* Delete dialog

* Adding tests

* New test

* Reverting unneeded change

* Removed console.log

* Tweaking email

* Reverting not applicable webhook changes

* Reverting dev email api

* Fixing last changes due to tests

* Changing user-evType relationship

* Availability and slug replacement tweaks

* Fixing event type delete

* Sometimes slug is not there...

* Removing old webhooks references
Changed slug hint

* Fixing types

* Fixing hiding event types actions

* Changing delete dialog text

* Removing unneeded code

* Applying feedback

* Update yarn.lock

* Making sure locked fields values are static

* Applying feedback

* Feedback + relying on children list, not users

* Removing console.log

* PR Feedback

* Telemetry for slug replacement action

* More unit tests

* Relying on schedule and editor tweaks

* Fixing conteiner classname

* PR Feedback

* PR Feedback

* Updating unit tests

* Moving stuff to ee, added feature flag

* type fix

* Including e2e

* Reverting unneeded changes in EmptyScreen

* Fixing some UI issues after merging tokens

* Fixing missing disabled locked fields

* Theme fixes + e2e potential fix

* Fixing e2e

* Fixing login relying on network

* Tweaking e2e

* Removing unneeded code

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-04-12 23:10:23 -03:00
Nafees NazikandHariom Balhara 7349fb9f6d test: add other fields to routing form e2e (#8112)
* chore: add test id

* chore: add test

* remove console log

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

* fix: linting

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-04-12 13:58:03 -07:00
Syed Ali Shahbaz a8368aac79 Caldav/refactor: Mutiple Fixes and General code improvement (#8031)
* code cleanup and fixes for unsupported objects and fetch requests

* improves sanitizer function

* adds comment to the utility function sanitizeCalendarObjects

* adds ICAL.parse in try catch-- fixes code breaking for all objects due to one bad ICAL body

* adds a function that ensures expand=false fallback for unsupporting calendars

* --WIP

* improves code to now support multi-event calendar ics parsing

* Handle failed calls to not break all selected calendars

* removes debug remnants

* removes further unnecessary comments
2023-04-12 14:30:39 -03:00
Abdallah AlsammanandAlex van Andel 91f381bce9 Replace react icons with lucidedev (#8146)
* migrate from react-icons to lucide-react

* replace react-icon with lucide-dev: Webhook Icon

* add lucide transformer

* Fix LinkIcon import

* Update yarn.lock to include monorepo deps

* Migrated icons in ChargeCardDialog

* Port Storybook to new icons as well

* Adjust Info & Globe icons size to match react-icons size

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-12 17:26:31 +02:00
Peer Richelsen fb9c1b18e0 remove leading-none (#8232) 2023-04-12 12:15:08 +00:00
Alex van Andel 1d18a09cc8 v2.7.15
release.yml / release (push) Canceled after 0s
2023-04-12 13:58:42 +02:00
Peer Richelsen 887d63ceef app store fixes (#8224) 2023-04-12 11:40:53 +00:00
Hariom Balhara 555fcc21f2 Revert color-scheme for embed (#8229) 2023-04-12 11:16:59 +00:00
Peer Richelsen f45fb03838 wip: replaced dark border with border-subtle (#8211) 2023-04-12 10:42:41 +02:00
Peer Richelsen 46611fdf77 make routing form full width (#8215) 2023-04-12 07:39:04 +05:30
Crowdin Bot c0f7c40bec New Crowdin translations by Github Action 2023-04-12 01:57:27 +00:00
Joe Au-Yeungandzomars f6f257c705 Refactor app store & fix admin apps list bugs (#7812)
* If omni install do not show app successfully installed toast

* Clean up. Up to hubspot

* Fix double click to enable apps

* Clean up rest of apps

* Create cli app add dirName

* Type fix

* Only enable apps if keys are valid

* Save dirName as slug

* Remove dirname from metadata

---------

Co-authored-by: zomars <zomars@me.com>
2023-04-11 18:56:43 -07:00
Crowdin Bot d61f50724b New Crowdin translations by Github Action 2023-04-11 22:14:21 +00:00
Alex van Andel 960aa0e00b v2.7.14
release.yml / release (push) Canceled after 0s
2023-04-12 00:10:27 +02:00
Joe Au-YeungandAlex van Andel 817e20f11e Stripe add the ability to place hold on cards (#8022)
* Add payment option to schema

* Add payment option to Stripe zod

* Set payment option on event type

* Create manual payment intent in Stripe

* Set payment option from Stripe app

* Add payment option to DB

* Pass React.ReactNode to checkbox

* Create uncaptured payment intent

* WIP

* Capture card in setup intent

* Show charge card option

* Charge card from booking page

* Bug fixes

* Clean up

* Clean up app card

* Add no-show fee messaging on booking page

* Send payment email on payment & add price

* Fix messaging

* Create no show fee charged email

* Send charge fee collected email

* Disable submit on card failure

* Clean up

* Serverside prevent charging card again if already charged

* Only confirm booking if paid for

* Type fixes

* More type fixes

* More type fixes

* Type fix

* Type fixes

* UI changes

* Payment component rework

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

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

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

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

* Update apps/web/components/dialog/ChargeCardDialog.tsx

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

* Update packages/trpc/server/routers/viewer/payments.tsx

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

* Revert GTM config

* Adjust payment option dropdown

* Show alert when seats are set

* Small bug fixes

* Create collect card method

* clean up

* Prevent seats & charge no-show fee to be enabled together

* Do not charge no-show fee on unconfirmed bookings

* Add check to collect card method

* Webhook send request emails

* Fix some dark mode colours

* Change awaiting payment language

* Type fixes

* Set height of Select and TextField both to 38px to fix alignment

* Fix message seats & payment error message

* Type fix

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-11 23:44:14 +02:00
Keith Williams af0123a606 Updated issue/PR staleness checks (#8209)
* Updated issue/PR staleness checks

* Changed order of settings to group them better
2023-04-11 22:39:53 +02:00
zomars 709bbf6626 Update yarn.lock 2023-04-11 11:41:48 -07:00
zomars 53097ee3bc Delete lockfile.yml 2023-04-11 11:40:13 -07:00
zomars 31f2002654 Update lockfile.yml 2023-04-11 11:38:20 -07:00
zomars@users.noreply.github.com ee155c3643 Update yarn.lock 2023-04-11 18:23:33 +00:00
zomars 8ab20c3c9f Update lockfile.yml 2023-04-11 11:21:17 -07:00
zomars 4f6d035d29 Create lockfile.yml 2023-04-11 11:07:55 -07:00
zomars 2e5b42ef47 Update yarn.lock 2023-04-11 10:56:30 -07:00
Alex van Andel b055672ea0 v2.7.13
release.yml / release (push) Canceled after 0s
2023-04-11 19:51:38 +02:00
Alex van Andel e4fbca388c Fix yarn.lock file 2023-04-11 19:50:36 +02:00
Joe Au-Yeung 2b8bba09c9 Prevent updating app to enabled on seed app store (#8092) 2023-04-11 21:51:21 +05:30
Alex van Andel 98cf29f655 Wrap useSuspense with NoSSR (#8079) 2023-04-11 21:49:27 +05:30
GitStart-Cal.com ae8706316f CALCOM-7895 - [CAL-1320] remove "application_fee_amount" for all regions (#7933) 2023-04-11 16:16:43 +00:00
Jeroen ReumkensandAlex van Andel ec8e08d442 Added serverside translations to teams page. (#7999)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-11 21:39:02 +05:30
sean-brydonandPeer Richelsen c33f424333 Fixes /more page + fixes multiple duration badge on BookingPage (#8200)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-11 15:21:24 +00:00
Hariom Balhara 6f2312d1e2 Fix app not visible in app-store (#8202) 2023-04-11 14:58:57 +00:00
Peer Richelsen c2b6070b7e Update package.json
release.yml / release (push) Canceled after 0s
2023-04-11 16:54:49 +02:00
Efraín Rochín a5bebe2b48 bugfix/sync-not-working-properly (#8168) 2023-04-11 16:51:27 +02:00
sean-brydon e3a1f20cc8 Darkmode Fixes (#8191) 2023-04-11 12:15:10 +00:00
sean-brydon ab639591da Fix forcing hard refresh across forced themes (#8188) 2023-04-11 17:20:25 +05:30
Hariom BalharaandPeer Richelsen 21a9b64eb0 Fix missing sylaps icon in admin/apps (#8190)
* Fi missing sylaps icon in admin/apps and remove usage of imageSrc

* Remove imageSrc from googleCalendar and verify that logo is still shown

* Use logo in video apps location select:

* Remove imageSrc from templates

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-11 11:38:37 +00:00
Omar López 636ee7b8a1 Prioritizes published teams over unpublished ones (#8177) 2023-04-11 06:16:57 +00:00
Crowdin Bot 543e526568 New Crowdin translations by Github Action 2023-04-11 02:21:03 +00:00
Hariom Balhara e465c1d71d Redirect on top iframe always - This is to make sure embed redirects on top frame (#8164) 2023-04-11 07:50:15 +05:30
Peer Richelsen cdab9037ce added dark mode upgrade banner (#8172) 2023-04-10 21:14:28 +00:00
Crowdin Bot 21e454d0e2 New Crowdin translations by Github Action 2023-04-10 20:27:50 +00:00
zomars 7384f29a1c Update crowdin.yml 2023-04-10 13:27:00 -07:00
Keith Williams 9016a854ca v2.7.11
release.yml / release (push) Canceled after 0s
2023-04-10 16:46:55 -03:00
Nafees Nazik 446c29dd9c fix: booker phone input style (#8165)
* fix: text area styles

* fix: phone input styles

* fix: use the text area component

* fix: remove focus style
2023-04-10 18:12:15 +02:00
Leandro Miguel NarcisoandPeer Richelsen 2cf1f07d8f Fix loading indicator alignment touching the select input value (#8083)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-09 11:16:33 +00:00
Nafees NazikandPeer Richelsen 4f03f8e66d feat: dynamically elapse time left timer (#7938)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-09 10:20:51 +00:00
e1096620e6 fix: replace ltr and rtl with new ms and me (#8138)
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-09 11:13:57 +02:00
sean-brydon d416386d7b Fix loading state - force subtle token to exist (#8153)
* Fix loading state - force subtle token to exist

* Add opacity
2023-04-08 18:50:17 +02:00
Keith Williams 5b7c55b029 v2.7.10
release.yml / release (push) Canceled after 0s
2023-04-07 17:04:56 -03:00
sean-brydon 9b05311d7f Fixes team skeleton - force brand on onboarding (#8142) 2023-04-07 19:28:06 +00:00
sean-brydon 313d25d7d8 Force lightmode (#8141) 2023-04-07 19:17:22 +00:00
Keith Williams 5ec9c41f7c v2.7.9 2023-04-07 15:29:01 -03:00
Nafees Nazik d56386d016 fix: dark mode variant in team member card (#8137) 2023-04-07 15:52:21 +00:00
sydwardraeandJoe Au-Yeung 0968ede733 fixed open-source to open source (#8127)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-04-07 09:15:13 +02:00
alannnc 8e3ed28ba9 Switch order from user/slug (#8134) 2023-04-07 09:13:22 +02:00
Nafees NazikandAlex van Andel 82ea4e9fb7 fix: switch bg when default checked (#8129)
* fix: switch bg

* Update packages/ui/components/form/switch/Switch.tsx

Remove accidental space

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-06 20:37:05 +00:00
Nafees Nazik 8a440c24c0 fix: email line height (#8130) 2023-04-06 17:38:13 +00:00
Alex van Andel 88a8da3ef6 Update lockfile 2023-04-06 17:59:17 +02:00
Nafees Nazik 49290fba72 fix: flash of title in event types page (#8114) 2023-04-06 12:25:54 +02:00
Udit Takkar a363b1e903 fix: ui and ux improvements in booking questions (#7734) 2023-04-06 08:17:53 +00:00
Carina WollendorferandCarinaWolli 7b5e2c8bab Don't schedule cancelled workflow reminders (#8096)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-04-06 13:15:01 +05:30
sean-brydon 4b0d22655d Feat/tokens + darkmode + theme support (#7876) 2023-04-05 18:14:46 +00:00
Peer Richelsen 4f92f60d85 added illustrations to app store (#8104) 2023-04-05 19:05:44 +02:00
Nafees Nazik 6986b5af40 Fix: availability not deleteable (#8086)
* fix: make availability deletable

* test: add e2e

* chore: add test id

* test: add a new test

* fix: remove utility
2023-04-05 12:42:22 -03:00
Alex van Andel e043a5efd8 Async import of the appStore packages (#8087) 2023-04-05 11:55:57 -03:00
Peer Richelsen bff28e5d6f discord app (#8101) 2023-04-05 14:07:18 +02:00
GitStart-Cal.comandgitstart-calcom f10ff18d9f FIX-CALCOM-8009 (#8097)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-04-05 11:54:25 +02:00
Nafees Nazik cefe4754c7 fix: add older variables to whitelist (#8095) 2023-04-05 10:35:32 +02:00
Omar López d7aa3c7163 v2.7.8
release.yml / release (push) Canceled after 0s
2023-04-04 16:58:13 -07:00
Peer Richelsen e899a92071 added turkish (#8091) 2023-04-05 00:13:00 +02:00
zomars ebdbe407cd Fixes crowding update 2023-04-04 12:43:00 -07:00
e7f917bab9 Feedback and improvements (#7903)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-04-04 10:35:51 -07:00
Hariom Balhara ba9688a04a BugFix: Don't open location dialog on removing location (#8085) 2023-04-04 14:43:41 +00:00
Stefan Florea f0e15d77dc feat: Google Tag Manager (GTM) App (#7952) 2023-04-04 17:53:11 +05:30
30b72b687e Follow up insights - user metrics (#8016)
* init page

* init insights frontend

* nit

* nit

* nit

* merge

* fixed icons

* i18n, needs features

* Init insights trpc

* Using trpc on client

* Added events timeline

* Seed analytics script

* connect ui with trpc

* Added and fixed event time lines

* WIP popular days and avg time duration event type

* added new metric graphs

* improved upgrade tip

* always show upgrade screen

* upgrade tremor.so and select inputs for page

* Remove log

* Move everything to components and add context

* Fix select types using calcom ui one

* Adding translations

* Add missing translations

* Add more translations

* min fix

* Fixes for date select

* Prefer early return and mobile design fixes

* Fix style for mobile

* Fix data with userId filter from popular events

* add user id to average time duration

* fix types for select-react

* Removed submodules

* Delete website

* Update yarn.lock

* Code organization and type fixes

* trpc fixes

* Builds are now passing

* Relocates server code

* Add url state in insights

* Update FiltersProvider.tsx

* Cleanup

* Update embed-iframe.ts

* Update FilterType.tsx

* Update seed-app-store.config.json

* Update index.tsx

* Renamed seeder

* Update FiltersProvider.tsx

* Fix for query params

* no wrap on lg screen

* Fix shadow borders from tremor components, fix title font

* Add ring-gray to match filters

* add cursor pointer

* copy improvements

* blue to black

* fixed date select focus

* text right for table

* Adds missing translation strings

* Fix url state for filter type

* more layout improvements

* more padding improvements

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

* Apply suggestions from code review

* WIP user without team metrics

* yarn

* Adds loading state for graphs and tables

* fix average render when no data

* Multiple fixes in insights

* Fix most and least booked members querys

* More fix for clear filters

---------

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

* Handle email text change
2023-04-04 07:55:56 +00:00
Harold ThétiotandHariom Balhara 24140f0f29 feat: add sylaps into the app-store in video category (#6728)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-04-04 13:12:07 +05:30
e504f5cd9e Fix/send responses everywhere customInputs were being sent. (#7898)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-04-04 10:29:09 +05:30
Alex van Andel 72629ffee0 v2.7.7
release.yml / release (push) Canceled after 0s
2023-04-03 19:47:10 +02:00
Nafees Nazik 5b6f007d8f fix: flickering of tips card rendering while changing routes (#8065)
* feat: optimize tips component

* fix: use dynamic rendering

* fix: remove unnecessary memo

* fix: prefetch warning

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

* Add iCalUID to booking event

* On reschedule write to evt the iCalUID

* Add uid to ics file

* Remove console logs

* Pass iCalUID if available

* Remove generated app store files

* Rename product id to calcom

* Add UID to ics on reschedule

* Type fixes

* Type fixes

* Type fixes

* Remove comment

* Remove console.log

* Removed serverConfig block from this branch

---------

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

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

* chore

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-03 16:37:58 +02:00
Jeroen ReumkensandAlex van Andel 99dba73d0b Updated Vercel OG to latest + added fallback img (#8057)
* Updated Vercel OG to latest so we can add a fallback image — and added this fallback image to the screenshot og image.

* Updated vercel/og in web package as well.

* Updated yarn.lock with auth dependencies

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

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

This reverts commit 4e9d97dbe5ebe682b905ce41f88197915e19647d.

* Added content type headers to og image api response.

---------

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

* fix: hydration error

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

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

* delete old batch_ids from scheduled sends

* only delete cancelled ones

---------

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

* feat: extract to an i18n key

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

---------

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

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

* feat: add new reservation status

* fix: add rescheduled to trpc procedure

* feat: use soft delete

* feat: add new i18n key

* feat: add data display for rescheduled booking

* chore: fix comment

* Revert "feat: add new i18n key"

This reverts commit 4cecc00bd3351af6fbdff05d728c438f68fbfc4e.

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

This reverts commit 81b4f7e1ab24763f630428b6c338926cf1958be0.

* Revert "fix: add rescheduled to trpc procedure"

This reverts commit 82b26582d2188e2b18f2f733760c741e7127b1d2.

* Revert "feat: add new reservation status"

This reverts commit b1dafab774d566dfcbe1c979035a89c729150d59.

* Revert "feat: add rescheduled status"

This reverts commit c8f24aec0e054b6be7186a631eee2b8a36ced40f.

* fix: use cancelled

* fix: e2e

* fix: e2e

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

* Renamed to DestinationCalendarSelectorComp

* Delete DestinationCalendarSelectorComp instead of rename

* Add DestinationCalendarSelect to CreateEventsOnCalendar

---------

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

* Add comment

---------

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

* feat: extract into a utility

* feat: use the new utility

* fix: comment out authedRateLimitedProcedure

* fix: type error

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

* Fix cloning emails

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

* Fixes?

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

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

* init insights frontend

* nit

* nit

* nit

* merge

* fixed icons

* i18n, needs features

* Init insights trpc

* Using trpc on client

* Added events timeline

* Seed analytics script

* connect ui with trpc

* Added and fixed event time lines

* WIP popular days and avg time duration event type

* added new metric graphs

* improved upgrade tip

* always show upgrade screen

* upgrade tremor.so and select inputs for page

* Remove log

* Move everything to components and add context

* Fix select types using calcom ui one

* Adding translations

* Add missing translations

* Add more translations

* min fix

* Fixes for date select

* Prefer early return and mobile design fixes

* Fix style for mobile

* Fix data with userId filter from popular events

* add user id to average time duration

* fix types for select-react

* Removed submodules

* Delete website

* Update yarn.lock

* Code organization and type fixes

* trpc fixes

* Builds are now passing

* Relocates server code

* Add url state in insights

* Update FiltersProvider.tsx

* Cleanup

* Update embed-iframe.ts

* Update FilterType.tsx

* Update seed-app-store.config.json

* Update index.tsx

* Renamed seeder

* Update FiltersProvider.tsx

* Fix for query params

* no wrap on lg screen

* Fix shadow borders from tremor components, fix title font

* Add ring-gray to match filters

* add cursor pointer

* copy improvements

* blue to black

* fixed date select focus

* Adds missing translation strings

* Fix url state for filter type

* Apply suggestions from code review

* Updated yarn lock

* Adds feature flag

* Type fix

---------

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

* Add env-cache

* Fix env setter

* Fix

* Another fix

* Fix

* Fix

* Fixes

* FFS

* Fix

* Fix

* Fix

* Fix

* Fix

* Cache fixes

* Fixes

* Adds skipping steps

* db-cache fixes

* Test

* Cache fixes

* e2e

* Possible caching conflicts

* Running out of ideas

* Caching is hard

* One more time

* cache-build not skipping

* Fingers crossed

* a

* Test

* Pls

* Please

* LFG

* Build fix

* fix

* Whitespace!!

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

* WIP

* Sends correct emails for paid bookings

* Update PaymentService.ts

* Update webhook.ts

* Update webhook.ts

* Update settings back button redirect link (#7403)

* fix(schedule): close on click #7143

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

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

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

* feat: get coutnry code from ip geolocation

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

* fix: create new api route for fetching code

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

* chore: replace city with country

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

* refactor: create hook for country

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

---------

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

* Team Workflows (#7038)

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

* Add destination calendar name to DestinationCalendarSelector (#6701)

* Add destination calendar name

* Type fix

* Search through calendars only for destination calendar credential

* Refactor get connected calendars

* Clean up

---------

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

* Update viewer.tsx (#7428)

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

* On booking add team members & translation

* Add team members to round robin create

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

* Send email on reschedules

* Send team email on cancelled event

* Add team members to calendar event description

* Clean up

* Convert other emails to organizer & teams

* Type check fixes

* More type fixes

* Change organizer scheduled input to an object

* early return updateCalendarEvent

* Introduce team member type

* Fix type errors

* Put team members before attendees

* Remove lodash cloneDeep

* Update packages/core/EventManager.ts

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

* Remove booking select object

* Revert "Remove booking select object"

This reverts commit 9f121ff4ebbaec9a0643111f96c5337b930ddd5b.

* Refactor email manager (#7270)

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

* Type change

* Remove conditional check for updateAllCalendarEvents

---------

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

* Typefix

* Updates webhook response

* Update pr.yml

* Update action.yml

* Update action.yml

* Update action.yml

* Update action.yml

* Update action.yml

* Is this redundant?

* Removed setup

* Update action.yml

* Update action.yml

* Consolitades setup step

* Revert "Consolitades setup step"

This reverts commit 5e8d1983cc5ac309fa6875a2fee77aa6287b7151.

* Fix?

* One more time

* Revert "One more time"

This reverts commit fd8b559a135820567f40dbb47a4b4dfc2ef44755.

* Benchmarking buildjet

* Update action.yml

* Re-introduce setup

* Adds embeds to missing pro cache

* Lint fixes

* Adds prettier ignore

* Upgrades to yarn 3

* Updates lockfile

* Reverts CI to ubuntu

* Testing new yarn install

* We cannot use immutable due to our private submodules

* Adds CI skip

* Fixes

* Adds plugin

* Forces local embed package

* Moves eslint to root

* Update yarn.lock

* Playwright fixes

* Embed test fixes

* Splits embed react tests

* Splits embed react tests

* Removes install step to benchmark

* Update playwright.config.ts

* One playwright config for all

* More test fixes

* Update basic.e2e.ts

* Added typescript as a global monorepo dev

* Update to v18

* Update yarn.lock

* Update env-create-file.yml

* Update .github/workflows/pr.yml

* Adds type check special for CI

* Update turbo.json

* Update turbo.json

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Esaú Morais <55207584+esau-morais@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-03-28 13:12:40 -07:00
72fd6334c6 Add the new Team->useHostDefault option (#7811)
* Add the new Team->useHostDefault option

* Added user.metadata to defaultEvents

* Type fixes

* Another type fix

* Implement feedback @jaibles

* Default -> Conferencing rename

---------

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

* Reuses cron endpoint to update team subscriptions

* Update cron-downgradeUsers.yml

* Update payments.ts

* Update cron-downgradeUsers.yml

* update to 90 writes per second

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

* Make it array

* Make api consistent and support calOrigin in floatingPopup and modal

* Support calOrigin everywhere and fix branding config issue

* Fix styles not being uniformly applied across components

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

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

* add list formatting

* add lost changes from merging main

* fixes caused by merge

* remove dompurify

* Revert "remove dompurify"

This reverts commit 583bb623c7d3c0c7daa28e63ab2b1ac2da68c0d8.

* sanitize already done in editor

* Update yarn.lock

---------

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

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

* Removing last schedule sets user.defaultScheduleId to null

* Create new schedule from event type

* First schedule created is the default

* Go back if new schedule was created

* Moved update logic out of getter

* Remove flashing of empty screen

* Add border prop to EmptyScreen

* User requires a schedule

* Change back schema

---------

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

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

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

---------

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

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

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

* fix: change hover border

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-27 16:35:46 +00:00
aa0e9e6a02 Prevent GCal from sending update (#7920)
* Do not send updates

* Clean up

* Clean up

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

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

---------

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

* fix type error

* Adjust tests

* fix type error

* include fix in e2e test

* fix e2e test

* fixes e2e test

* fix type error in e2e test

* increase timeout time

---------

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

* Remove menu is open

* Timezone fixes

---------

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

* Add error message

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

* Create router.ts

* WIP

* WIP

* WIP

* WIP

* Emails kill switch

* Adds missing migrations

* Type fix

* Cleanup

* Revert

* Cleanup

* Fixes migration

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

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

---------

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

* Set min of booking notice to 0

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

* init insights frontend

* nit

* nit

* nit

* merge

* fixed icons

* i18n, needs features

* Init insights trpc

* Using trpc on client

* Added events timeline

* Seed analytics script

* connect ui with trpc

* Added and fixed event time lines

* WIP popular days and avg time duration event type

* added new metric graphs

* improved upgrade tip

* always show upgrade screen

* upgrade tremor.so and select inputs for page

* Remove log

* Move everything to components and add context

* Fix select types using calcom ui one

* Adding translations

* Add missing translations

* Add more translations

* min fix

* Fixes for date select

* Prefer early return and mobile design fixes

* Fix style for mobile

* Fix data with userId filter from popular events

* add user id to average time duration

* Removed submodules

* Delete website

* Update yarn.lock

* Code organization and type fixes

* trpc fixes

* Builds are now passing

* Relocates server code

* Cleanup

* Medium size screen fixes

* Added TODO

---------

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

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

* Remove 'crypto' from client code

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

This reverts commit ecc2038411c73fc562d57712d5c83d66925e7e13.

* Adding lodash to dynamicImports + consistent imports

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

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

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

* fix: create a common function

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

* fix: add suggestion

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

* chore: use deconstructing

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

* chore

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

---------

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

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

* add styling for lists

* sanitize editor input before saving

* sanitize eventTypeDescription

* santize html when used dangerouslySetInnerHTML

* fix server side sanitation

* add missing formatting and sanitation

* move @ts-expect-error to correct line

* fix type error and add yarn.lock

* fix build error

* sanitize description for booking page and availability page

* rename to markdownAndSanitize

* always add list formatting

* handle empty description in markdownAndSanitize for cleaner code

* create function for clientside markdown rendering and sanitizing

* fix type error

* code clean up

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

* Remove unused variable

* Remove one more ts-expect-error

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-23 12:00:42 +00:00
Nafees Nazik 520e7fe036 fix: remove "Nameless" from title in booking cancel page (#7888) 2023-03-23 10:22:30 +00:00
Nafees Nazik 1bf50e09cd fix: meeting not getting deleted (#7878) 2023-03-22 11:18:00 -07:00
darshanandBailey Pumfleet d1ca7b4bdd fix redirect for recurring events (#7821)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-03-22 11:22:40 -04:00
Richard Poelderl 4e2473531f fix: set canonical to https://cal.com on .dev as well (#7883) 2023-03-22 15:43:59 +01:00
Peer Richelsenandnafees nazik efb9bf7116 replacing intercom with helpscout (#7865)
release.yml / release (push) Canceled after 0s
Co-authored-by: nafees nazik <nafeesnazik21@gmail.com>
2023-03-22 14:29:14 +00:00
49ee5d300b Fix rescheduling updating calendars & duplicate bookings (#7851)
* Fix rescheduling

* Fix tests

* Clean up

* Update packages/core/EventManager.ts

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

* Commit fix

---------

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

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

* fix: add zod schema, fix type eroor

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

---------

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

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

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

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

* chore: change name of variable

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

---------

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

* iCal & Apple add team members

* Exchange add team members

* iCal & App cal update include attendees

* Exchange reschedule

* GCal add team members on reschedule

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

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

* remove $ prefix

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

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

* pass responses in same format

---------

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

* figma 1-1

* Style X

* Fixes timezone select

* Add location select styles

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

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

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

---------

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

* figma 1-1

* Style X

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

* fix: add dangerously set inner html

* fix: use markdown

* fix: add remove html regex

* fix: revert dangerouslySetInnerHTML

* fix: title

* added yarn.lock

* yarn

* fix: yarn lock

* Update yarn.lock

* Update yarn lock

* Revert yarn lock changes

---------

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

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

* code clean up

---------

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

* send start and end date in UTC

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

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

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

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

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

---------

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

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

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

* Fixed linting

* Add seats logic to reschedule

---------

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

* fix: revert padding top and bottom

* fix: remove large prop

* fix: remove large prop

* feat: make md default

---------

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

* chore : wrap actions inside useMemo

---------

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

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

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

---------

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

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

* Clean up console.logs

* Push into attendeesData

---------

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

* Add env-cache

* Fix env setter

* Fix

* Another fix

* Fix

* Fix

* Fixes

* FFS

* Fix

* Fix

* Fix

* Fix

* Fix

* Cache fixes

* Fixes

* Adds skipping steps

* db-cache fixes

* Test

* Cache fixes

* e2e

* Possible caching conflicts

* Running out of ideas

* Caching is hard

* One more time

* cache-build not skipping

* Fingers crossed

* a

* Test

* Pls

* Please

* LFG

* Build fix

* fix

* Whitespace!!

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

* WIP

* Sends correct emails for paid bookings

* Update PaymentService.ts

* Update webhook.ts

* Update webhook.ts

* Update settings back button redirect link (#7403)

* fix(schedule): close on click #7143

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

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

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

* feat: get coutnry code from ip geolocation

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

* fix: create new api route for fetching code

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

* chore: replace city with country

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

* refactor: create hook for country

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

---------

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

* Team Workflows (#7038)

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

* Add destination calendar name to DestinationCalendarSelector (#6701)

* Add destination calendar name

* Type fix

* Search through calendars only for destination calendar credential

* Refactor get connected calendars

* Clean up

---------

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

* Update viewer.tsx (#7428)

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

* On booking add team members & translation

* Add team members to round robin create

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

* Send email on reschedules

* Send team email on cancelled event

* Add team members to calendar event description

* Clean up

* Convert other emails to organizer & teams

* Type check fixes

* More type fixes

* Change organizer scheduled input to an object

* early return updateCalendarEvent

* Introduce team member type

* Fix type errors

* Put team members before attendees

* Remove lodash cloneDeep

* Update packages/core/EventManager.ts

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

* Remove booking select object

* Revert "Remove booking select object"

This reverts commit 9f121ff4ebbaec9a0643111f96c5337b930ddd5b.

* Refactor email manager (#7270)

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

* Type change

* Remove conditional check for updateAllCalendarEvents

---------

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

* Typefix

* Updates webhook response

* Update pr.yml

* Update action.yml

* Update action.yml

* Update action.yml

* Update action.yml

* Update action.yml

* Is this redundant?

* Removed setup

* Update action.yml

* Update action.yml

* Consolitades setup step

* Revert "Consolitades setup step"

This reverts commit 5e8d1983cc5ac309fa6875a2fee77aa6287b7151.

* Fix?

* One more time

* Revert "One more time"

This reverts commit fd8b559a135820567f40dbb47a4b4dfc2ef44755.

* Benchmarking buildjet

* Update action.yml

* Re-introduce setup

* Adds embeds to missing pro cache

* Lint fixes

* Adds prettier ignore

* Upgrades to yarn 3

* Updates lockfile

* Reverts CI to ubuntu

* Testing new yarn install

* We cannot use immutable due to our private submodules

* Adds CI skip

* Fixes

* Adds plugin

* Forces local embed package

* Moves eslint to root

* Update yarn.lock

* Playwright fixes

* Embed test fixes

* Splits embed react tests

* Splits embed react tests

* Removes install step to benchmark

* Update playwright.config.ts

* One playwright config for all

* More test fixes

* Update basic.e2e.ts

* Added typescript as a global monorepo dev

* Update to v18

* Update yarn.lock

* Update env-create-file.yml

* Update .github/workflows/pr.yml

---------

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

* Fix for pipeline

* Update gh.env

* Update env-create-file.yml

* Reverts some changes

* Update turbo.json

* Update .env.example

---------

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

* fix(web): support node 19 as accepted

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

* Drop support for Node 16.x

* Removed n19 pending @azure/msal-node

---------

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

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

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

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

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

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

* update images

* update README with zoho integration

* Fix dirname

* Fix types

* Fix lastname bug

* Fix timezone issue

* Fix eslint warning for unused args

* Revert yarn.lock

---------

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

* fix: use the camel cased startIcon prop

* fix: reschedule request badge

* fix: use the camel cased startIcon prop

* fix: remove bold prop

* Removed extra whitespace in HTML

---------

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

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

* Remove logs and created test

* WIP saving progress

* Select second slot to pass test

* Delete attendee from event

* Clean up

* Update with main changes

* Fix emails not being sent

* Changed test end url from success to booking

* Remove unused pkg

* Fix new booking reschedule

* remove log

* Renable test

* remove unused pkg

* rename table name

* review changes

* Fix and and other test to reschedule with seats

* Fix api for cancel booking

* Typings

* Update [uid].tsx

* Abstracted common pattern

into maybeGetBookingUidFromSeat

* Reverts

* Nitpicks

* Update handleCancelBooking.ts

* Adds missing cascades

* Improve booking seats changes (#6858)

* Create sendCancelledSeatEmails

* Draft attendee cancelled seat email

* Send no longer attendee email to attendee

* Send email to organizer when attendee cancels

* Pass cloned event data to emails

* Send booked email for first seat

* Add seat reference uid from email link

* Query for seatReferenceUId and add to cancel & reschedule links

* WIP

* Display proper attendee when rescheduling seats

* Remove console.logs

* Only check for already invited when not rescheduling

* WIP sending reschedule email to just single attendee and owner

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

* Remove console.logs

* Add cloned event to seat emails

* Do not show manage link for calendar event

* First seat, have both attendees on calendar

* WIP refactor booking seats reschedule logic

* WIP Refactor handleSeats

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

* Migration with relationship change

* Booking page handling unique seat references

* Abstract to handleSeats

* Remove console.logs and clean up

* New migration file, delete on cascade

* Check if attendee is already a part of the booking

* Move deleting booking logic to `handleSeats`

* When owner reschedule, move whole booking

* Prevent owner from rescheduling if not enough seats

* Add owner reschedule

* Send reschedule email when moving to new timeslot

* Add event data to reschedule email for seats

* Remove DB changes from event manager

* When a booking has no attendees then delete

* Update calendar when merging bookings

* Move both attendees and seat references when merging

* Remove guest list from seats booking page

* Update original booking when moving an attendee

* Delete calendar and video events if no more attendees

* Update or delete integrations when attendees cancel

* Show no longer attendee if a single attendee cancels

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

* If booking in same slot then just return the booking

* Clean up

* Clean up

* Remove booking select

* Typos

---------

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

* Fix migration table name

* Add missing trpc import

* Rename bookingSeatReferences to bookingSeat

* Change relationship between Attendee & BookingSeat to one to one

* Fix some merge conflicts

* Minor merge artifact fixup

* Add the right 'Person' type

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

* Removed calEvent.attendeeUniqueId

* rename referenceUId -> referenceUid

* Squashes migrations

* Run cached installs

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

* Solve attendee form on booking page

* Remove unused code

* Some type fixes

* Squash migrations

* Type fixes

* Fix for reschedule/[uid] redirect

* Fix e2e test

* Solve double declaration of host

* Solve lint errors

* Drop constraint only if exists

* Renamed UId to Uid

* Explicit vs. implicit

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

* Various bugfixes

* Persistently apply seatReferenceUid (#7545)

* Persistently apply seatReferenceUid

* Small ts fix

* Setup guards correctly

* Type fixes

* Fix render 0 in conditional

* Test refactoring

* Fix type on handleSeats

* Fix handle seats conditional

* Fix type inference

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

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

* Fix type and missing logic for reschedule

* Fix delete of calendar event and booking

* Add handleSeats return type

* Fix seats booking creation

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

* Simplification of fetching booking

* Enable seats for round-robin events

* A lot harder than I expected

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

* Return seatReferenceUid when second seat

* negate userIsOwner

* Fix booking seats with a link without bookingUid

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

* Can't open dialog twice in test..

* Allow passing the booking ID from the server

* Fixed isCancelled check, fixed test

* Delete through cascade instead of multiple deletes

---------

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

* re add space

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

---------

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

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

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

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

* Adds backend logic

* Type fixes

---------

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

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

* fix: remove is_production

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

* wip

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

* style: calltoAction

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

* fix: brightness

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

* fix: styling of button

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-12 17:39:46 +00:00
sean-brydon a22e4eed32 Fixes weird background color (#7678) 2023-03-12 15:54:03 +00:00
Keith Williams 1625f462c2 Fixed linting issues in Playwright config (#7662) 2023-03-12 09:39:08 +01:00
919 changed files with 66349 additions and 39954 deletions
+13 -1
View File
@@ -15,16 +15,22 @@
# - LARK
# - WEB3
# - SALESFORCE
# - ZOHOCRM
# - APP STORE **********************************************************************************************
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
# - DAILY.CO VIDEO
# Enables Cal Video. to get your key
# 1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
# 2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
# @see https://github.com/calcom/cal.com#obtaining-daily-api-credentials
DAILY_API_KEY=
DAILY_SCALE_PLAN=''
# - GOOGLE CALENDAR/MEET/LOGIN
# Needed to enable Google Calendar integration and Login with Google
# @see https://github.com/calendso/calendso#obtaining-the-google-api-credentials
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
GOOGLE_API_CREDENTIALS=
# To enable Login with Google you need to:
@@ -109,4 +115,10 @@ INFURA_API_KEY=""
# Used for the Salesforce (Sales Cloud) app
SALESFORCE_CONSUMER_KEY=""
SALESFORCE_CONSUMER_SECRET=""
# - ZOHOCRM
# Used for the Zoho CRM integration
ZOHOCRM_CLIENT_ID=""
ZOHOCRM_CLIENT_SECRET=""
# *********************************************************************************************************
+9 -3
View File
@@ -22,6 +22,13 @@ CALCOM_LICENSE_KEY=
# - DATABASE ************************************************************************************************
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
# PRISMA_GENERATE_DATAPROXY=true
PRISMA_GENERATE_DATAPROXY=
# ***********************************************************************************************************
# - SHARED **************************************************************************************************
@@ -153,9 +160,6 @@ CLOSECOM_API_KEY=
# Sendgrid internal sync service
SENDGRID_SYNC_API_KEY=
# Sentry
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_IGNORE_API_RESOLUTION_ERROR=
# Change your Brand
NEXT_PUBLIC_APP_NAME="Cal.com"
@@ -170,3 +174,5 @@ CSP_POLICY=
# Vercel Edge Config
EDGE_CONFIG=
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
+6
View File
@@ -0,0 +1,6 @@
node_modules
**/**/node_modules
**/**/.next
**/**/public
packages/prisma/zod
apps/web/public/embed
+2 -1
View File
@@ -5,7 +5,8 @@
# the repo. Unless a later match takes precedence,
# @global-owner1 and @global-owner2 will be requested for
# review when someone opens a pull request.
* @calcom/reviewers
# ⬇️ Removed so we can manually trigger checks
# * @calcom/reviewers
.github/ @calcom/core
.husky/ @calcom/core
+6 -6
View File
@@ -12,13 +12,13 @@ Fixes # (issue)
## Type of change
<!-- Please delete options that are not relevant. -->
<!-- Please delete bullets that are not relevant. -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- Bug fix (non-breaking change which fixes an issue)
- Chore (refactoring code, technical debt, workflow improvements)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
- This change requires a documentation update
## How should this be tested?
+2 -1
View File
@@ -3,7 +3,7 @@ description: "Cache or restore if necessary"
inputs:
node_version:
required: false
default: v16.x
default: v18.x
runs:
using: "composite"
steps:
@@ -20,6 +20,7 @@ runs:
with:
path: |
${{ github.workspace }}/apps/web/.next
${{ github.workspace }}/apps/web/public/embed
**/.turbo/**
**/dist/**
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
+49 -6
View File
@@ -1,9 +1,21 @@
name: Yarn install
description: "Install all NPM dependencies, caches them and restores if necessary"
########################################################################################
# "yarn install" composite action for yarn 2/3/4+ and "nodeLinker: node-modules" #
#--------------------------------------------------------------------------------------#
# Cache: #
# - Downloaded zip archive (multi-arch, preserved across yarn.lock changes) #
# - Yarn install state (discarded on yarn.lock changes) #
# References: #
# - bench: https://gist.github.com/belgattitude/0ecd26155b47e7be1be6163ecfbb0f0b #
# - vs @setup/node: https://github.com/actions/setup-node/issues/325 #
########################################################################################
name: "Yarn install"
description: "Run yarn install with node_modules linker and cache enabled"
inputs:
node_version:
required: false
default: v16.x
default: v18.x
runs:
using: "composite"
steps:
@@ -11,9 +23,40 @@ runs:
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node_version }}
cache: "yarn"
- name: Yarn install
- name: Expose yarn config as "$GITHUB_OUTPUT"
id: yarn-config
shell: bash
run: |
yarn install --prefer-offline --frozen-lockfile
echo "CACHE_FOLDER=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
# Yarn rotates the downloaded cache archives, @see https://github.com/actions/setup-node/issues/325
# Yarn cache is also reusable between arch and os.
- name: Restore yarn cache
uses: actions/cache@v3
id: yarn-download-cache
with:
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
key: yarn-download-cache-${{ hashFiles('yarn.lock') }}
restore-keys: |
yarn-download-cache-
# Invalidated on yarn.lock changes
- name: Restore yarn install state
id: yarn-install-state-cache
uses: actions/cache@v3
with:
path: .yarn/ci-cache/
key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
- name: Install dependencies
shell: bash
run: |
yarn install --inline-builds
yarn prisma generate
env:
# CI optimizations. Overrides yarnrc.yml options (or their defaults) in the CI action.
YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # So it doesn't try to remove our private submodule deps
YARN_ENABLE_GLOBAL_CACHE: "false" # Use local cache folder to keep downloaded archives
YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz # Very small speedup when lock does not change
# Other environment variables
HUSKY: "0" # By default do not run HUSKY install
+18
View File
@@ -0,0 +1,18 @@
{
"problemMatcher": [
{
"owner": "tsc-absolute",
"pattern": [
{
"regexp": "(?:^|\\s)([^\\s].*)[\\(:](\\d+)[,:](\\d+)(?:\\):\\s+|\\s+-\\s+)(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"code": 5,
"message": 6
}
]
}
]
}
@@ -0,0 +1,27 @@
name: "Apply issue labels to PR"
on:
pull_request:
types: [opened]
jobs:
label_on_pr:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 16
- name: Apply labels from linked issue to PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_DATA: ${{ toJson(github.event.pull_request) }}
run: node ./.github/workflows/scripts/apply-issue-labels-to-pr.ts
+5 -3
View File
@@ -10,8 +10,10 @@ jobs:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- name: Show info
run: node -e "console.log(require('v8').getHeapStatistics())"
- run: yarn type-check
- name: Configure TSC problem matcher
run: |
echo "::remove-matcher owner=tsc::"
echo "::add-matcher::.github/matchers/tsc-absolute.json"
- run: yarn type-check:ci
@@ -1,20 +0,0 @@
name: Cleanup Preview Databases
env:
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
SNAPLET_PROJECT_ID: cl4u26bwz7962859ply7ibuo43t
on:
pull_request:
types: [closed]
branches:
- main
jobs:
delete:
runs-on: ubuntu-latest
steps:
- uses: snaplet/action@v1
if: ${{ env.SNAPLET_ACCESS_TOKEN && env.SNAPLET_PROJECT_ID }}
with:
delete: true
+3 -2
View File
@@ -1,11 +1,12 @@
name: Cron - downgradeUsers
on:
workflow_dispatch:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
# Runs “Every month at 1st (see https://crontab.guru)
- cron: "0 0 1 * *"
jobs:
cron-downgradeUsers:
env:
+6 -5
View File
@@ -15,8 +15,9 @@ jobs:
steps:
- uses: actions/stale@v7
with:
days-before-stale: 60
include-only-assigned: true
days-before-close: -1
days-before-issue-stale: 60
days-before-issue-close: -1
days-before-pr-stale: 14
days-before-pr-close: 7
stale-pr-message: "This PR is being marked as stale due to inactivity."
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
+9 -4
View File
@@ -15,14 +15,19 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: crowdin action
uses: crowdin/github-action@1.4.9
uses: crowdin/github-action@1.5.1
with:
upload_translations: true
upload_sources: true
download_translations: true
pull_request_labels: "♻️ autoupdate, automerge"
push_translations: true
commit_message: "New Crowdin translations by Github Action"
localization_branch_name: main
create_pull_request: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
-37
View File
@@ -1,37 +0,0 @@
# .github/workflows/preview.yml
name: Preview Environment
env:
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
SNAPLET_PROJECT_ID: ${{ secrets.SNAPLET_PROJECT_ID }}
VERCEL_ACCESS_TOKEN: ${{ secrets.VERCEL_ACCESS_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
on:
pull_request:
types: [opened, synchronize, closed]
branches:
- main
jobs:
db-preview-deploy:
if: ${{ github.event.action == 'opened' || github.event.action == 'synchronize' }}
runs-on: ubuntu-latest
steps:
- id: snaplet
uses: snaplet/action@v1
- uses: snaplet/vercel-action@v1
with:
env: |
DATABASE_URL=${{ steps.snaplet.outputs.database-url }}
db-preview-delete:
if: ${{ github.event.action == 'closed' }}
runs-on: ubuntu-latest
steps:
- uses: snaplet/action@v1
with:
delete: true
- uses: snaplet/vercel-action@v1
with:
delete: true
-18
View File
@@ -1,18 +0,0 @@
name: Staging DB Snapshot
on:
schedule:
- cron: "0 4 * * 1-5" # At 04:00 on every day-of-week from Monday through Friday.
workflow_dispatch:
jobs:
db-staging-snapshot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Snaplet CLI
run: curl -sL https://app.snaplet.dev/get-cli/ | bash
- name: Restore Snapshot
run: snaplet snapshot restore -y
env:
SNAPLET_DATABASE_URL: ${{ secrets.SNAPLET_DATA_TARGET_DB_URL }}
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
SNAPLET_DATABASE_ID: ${{ secrets.SNAPLET_DATABASE_ID }}
+36
View File
@@ -0,0 +1,36 @@
name: E2E Embed tests and booking flow(for non-embed as well)
on:
workflow_call:
jobs:
e2e-embed:
timeout-minutes: 20
runs-on: ubuntu-latest
services:
postgres:
image: postgres:12.1
env:
POSTGRES_USER: postgres
POSTGRES_DB: calendso
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- uses: ./.github/actions/env-read-file
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn test-e2e:embed-react
env:
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results
path: test-results
+4 -11
View File
@@ -25,19 +25,12 @@ jobs:
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
- name: Run Tests
run: yarn turbo run embed-tests-update-snapshots:ci --scope=@calcom/embed-react --concurrency=1
run: yarn test-e2e:embed
env:
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
- name: Upload embed-core results
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results-embed-core
path: packages/embeds/embed-core/playwright/results
- name: Upload embed-react results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results-embed-react
path: packages/embeds/embed-react/playwright/results
name: test-results
path: test-results
+2 -1
View File
@@ -20,7 +20,7 @@ env:
INPUT_ENV_PAYMENT_FEE_FIXED: 10
INPUT_ENV_SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
INPUT_ENV_SAML_ADMINS: pro@example.com
INPUT_ENV_NEXTAUTH_URL: http://localhost:3000/api/auth
INPUT_ENV_NEXTAUTH_URL: http://127.0.0.1:3000/api/auth
INPUT_ENV_NEXT_PUBLIC_IS_E2E: 1
# INPUT_ENV_EMAIL_FROM: e2e@cal.com
# INPUT_ENV_EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
@@ -39,6 +39,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ozaytsev86/create-env-file@v1
with:
file-name: ${{ github.workspace }}/gh.env
+11
View File
@@ -21,6 +21,7 @@ jobs:
outputs:
app-store: ${{ steps.filter.outputs.app-store }}
embed: ${{ steps.filter.outputs.embed }}
embed-react: ${{ steps.filter.outputs.embed-react }}
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
@@ -34,6 +35,9 @@ jobs:
embed:
- 'apps/web/**'
- 'packages/embeds/**'
embed-react:
- 'apps/web/**'
- 'packages/embeds/**'
env:
name: Create env file
@@ -81,6 +85,13 @@ jobs:
uses: ./.github/workflows/e2e-embed.yml
secrets: inherit
e2e-embed-react:
name: E2E React embeds tests
if: ${{ needs.changes.outputs.embed-react == 'true' }}
needs: [changes, lint, build]
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
analyze:
needs: build
uses: ./.github/workflows/nextjs-bundle-analysis.yml
@@ -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);
})();
+9
View File
@@ -87,3 +87,12 @@ apps/api
apps/website
apps/console
apps/auth
# Yarn Modern
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+1 -1
View File
@@ -1 +1 @@
v16
v18
+1
View File
@@ -14,3 +14,4 @@ public
.DS_Store
.eslintignore
packages/prisma/zod
apps/web/public/embed
File diff suppressed because one or more lines are too long
+873
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.4.1.cjs
+16 -3
View File
@@ -88,6 +88,7 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
- [React](https://reactjs.org/?ref=cal.com)
- [Tailwind](https://tailwindcss.com/?ref=cal.com)
- [Prisma](https://prisma.io/?ref=cal.com)
- [Daily.co](https://go.cal.com/daily)
## Stay Up-to-Date
@@ -349,8 +350,7 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
<source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=dark">
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
</picture>
</a>
</a>
<!-- CONTRIBUTORS -->
@@ -432,7 +432,7 @@ following
### Obtaining Daily API Credentials
1. Open [Daily](https://www.daily.co/) and sign into your account.
1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
3. Copy your API key.
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
@@ -451,6 +451,19 @@ following
9. Click the "Save" button at the bottom footer.
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
### Obtaining ZohoCRM Client ID and Secret
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
2. From within the API console page, go to "Applications".
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
4. Fill in any information you want in the "Client Details" tab
5. Go to tab "Client Secret" tab.
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
9. Click the "Save"/ "UPDATE" button at the bottom footer.
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
## Workflows
### Setting up SendGrid for Email reminders
+1 -1
View File
@@ -1 +1 @@
14.17
v18
+3 -3
View File
@@ -11,7 +11,8 @@
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
"start": "PORT=4000 next start",
"type-check": "tsc --pretty --noEmit"
"type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit"
},
"author": "Cal.com, Inc.",
"license": "MIT",
@@ -24,7 +25,6 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@calcom/config": "*",
"eslint": "^8.34.0"
"@calcom/config": "*"
}
}
+3 -2
View File
@@ -1,4 +1,5 @@
import React, { ReactElement, ReactNode } from "react";
import type { ReactElement, ReactNode } from "react";
import React from "react";
import { classNames } from "@calcom/lib";
@@ -42,7 +43,7 @@ export function VariantsTable({
</table>
</div>
{!isDark && (
<div data-mode="dark">
<div data-mode="dark" className="dark">
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
{children}
</VariantsTable>
@@ -193,3 +193,88 @@
.docs-story {
padding: 24px !important;
}
@layer {
:root {
/* background */
--cal-bg-emphasis: #E5E7EB;
--cal-bg: white;
--cal-bg-subtle: #F3F4F6;
--cal-bg-muted: #F9FAFB;
--cal-bg-inverted: #111827;
/* background -> components*/
--cal-bg-info: #DEE9FC;
--cal-bg-success: #E2FBE8;
--cal-bg-attention: #FCEED8;
--cal-bg-error: #F9E3E2;
/* Borders */
--cal-border-emphasis:#9CA3AF;
--cal-border: #D1D5DB;
--cal-border-subtle:#E5E7EB;
--cal-border-muted:#F3F4F6;
/* Content/Text */
--cal-text-emphasis: #111827;
--cal-text:#374151;
--cal-text-subtle:#6B7280;
--cal-text-muted:#9CA3AF;
--cal-text-inverted:white;
/* Content/Text -> components */
--cal-text-info:#253985;
--cal-text-success:#285231;
--cal-text-attention:#73321B;
--cal-text-error:#752522;
/* Brand shinanigans
-> These will be computed for the users theme at runtime.
*/
--cal-brand:#111827;
--cal-brand-emphasis:#101010;
}
.dark {
/* background */
--cal-bg-emphasis: #2B2B2B;
--cal-bg: #101010;
--cal-bg-subtle: #2B2B2B;
--cal-bg-muted: #1C1C1C;
--cal-bg-inverted: #F3F4F6;
/* background -> components*/
--cal-bg-info: #DEE9FC;
--cal-bg-success: #E2FBE8;
--cal-bg-attention: #FCEED8;
--cal-bg-error: #F9E3E2;
/* Borders */
--cal-border-emphasis: #575757;
--cal-border: #444444;
--cal-border-subtle: #2B2B2B;
--cal-border-muted: #1C1C1C;
/* Content/Text */
--cal-text-emphasis: #F3F4F6;
--cal-text: #D6D6D6;
--cal-text-subtle: #757575;
--cal-text-muted: #575757;
--cal-text-inverted: #101010;
/* Content/Text -> components */
--cal-text-info: #253985;
--cal-text-success: #285231;
--cal-text-attention: #73321B;
--cal-text-error: #752522;
/* Brand shenanigans
-> These will be computed for the users theme at runtime.
*/
--cal-brand: #111827;
--cal-brand-emphasis: #101010;
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
node_modules
prisma/zod
public/embed
-2
View File
@@ -68,5 +68,3 @@ public/embed
# Copied app-store images
public/app-store
# Sentry
.sentryclirc
+1 -1
View File
@@ -1 +1 @@
v16
v18
+1
View File
@@ -0,0 +1 @@
public/embed
-53
View File
@@ -103,59 +103,6 @@ paths:
summary: Reset a user's password
tags:
- Authentication
"/api/availability/{user}?dateFrom={dateFrom}&dateTo={dateTo}":
get:
description: "Gets the busy times for a particular user, by username."
summary: Gets the busy times for a user
tags:
- Availability
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
description: ""
minItems: 1
uniqueItems: true
x-examples:
example-1:
- start: "Fri, 03 Sep 2021 17:00:00 GMT"
end: "Fri, 03 Sep 2021 17:40:00 GMT"
items:
type: object
properties:
start:
type: string
minLength: 1
end:
type: string
minLength: 1
required:
- start
- end
"500":
description: Internal Server Error
parameters:
- schema:
type: string
name: user
in: path
required: true
description: The username of who you want to check availability for
- schema:
type: string
name: dateFrom
in: path
required: true
description: The timestamp of which time you want to get busy times from
- schema:
type: string
name: dateTo
in: path
required: true
description: The timestamp of which time you want to get busy times until
/api/availability/calendar:
get:
description: Gets the user's selected calendars.
+3 -3
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { FiX } from "@calcom/ui/components/icon";
import { X } from "@calcom/ui/components/icon";
export default function AddToHomescreen() {
const { t } = useLocale();
@@ -29,7 +29,7 @@ export default function AddToHomescreen() {
<path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z" />
</svg>
</span>
<p className="text-xs font-medium text-white ltr:ml-3 rtl:mr-3">
<p className="text-inverted ms-3 text-xs font-medium">
<span className="inline">{t("add_to_homescreen")}</span>
</p>
</div>
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
type="button"
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
<span className="sr-only">{t("dismiss")}</span>
<FiX className="h-6 w-6 text-white" aria-hidden="true" />
<X className="text-inverted h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
@@ -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;
+4 -4
View File
@@ -6,7 +6,7 @@ import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { Badge, ListItemText } from "@calcom/ui";
import { FiAlertCircle } from "@calcom/ui/components/icon";
import { AlertCircle } from "@calcom/ui/components/icon";
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
@@ -63,12 +63,12 @@ export default function AppListCard(props: AppListCardProps) {
}, []);
return (
<div className={`${highlight ? "bg-yellow-100" : ""}`}>
<div className={`${highlight ? "dark:bg-muted bg-yellow-100" : ""}`}>
<div className="flex gap-x-3 px-5 py-4">
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
<div className="flex grow flex-col gap-y-1 truncate">
<div className="flex items-center gap-x-2">
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
<h3 className="text-emphasis truncate text-sm font-semibold">{title}</h3>
<div className="flex items-center gap-x-2">
{isDefault && <Badge variant="green">{t("default")}</Badge>}
{isTemplate && <Badge variant="red">Template</Badge>}
@@ -77,7 +77,7 @@ export default function AppListCard(props: AppListCardProps) {
<ListItemText component="p">{description}</ListItemText>
{invalidCredential && (
<div className="flex gap-x-2 pt-2">
<FiAlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
<AlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
{t("invalid_credential")}
</ListItemText>
@@ -1,147 +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) }),
option: (defaultStyles, state) => ({
...defaultStyles,
backgroundColor: state.isSelected
? state.isFocused
? "var(--brand-color)"
: "var(--brand-color)"
: state.isFocused
? "var(--brand-color-dark-mode)"
: "var(--brand-text-color)",
}),
control: (defaultStyles) => {
return {
...defaultStyles,
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;
+182 -182
View File
@@ -1,4 +1,4 @@
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
import classNames from "classnames";
import type { NextRouter } from "next/router";
import { useRouter } from "next/router";
@@ -22,7 +22,7 @@ import {
TextField,
ColorPicker,
} from "@calcom/ui";
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
import { Code, Trello, Sun, ArrowLeft } from "@calcom/ui/components/icon";
import Select from "@components/ui/form/Select";
@@ -167,7 +167,7 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
const height = getDimension(previewState.inline.height);
return code`
import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function MyComponent() {
useEffect(()=>{
(async function () {
@@ -187,12 +187,12 @@ function MyComponent() {
}) => {
return code`
import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function MyComponent() {
useEffect(()=>{
(async function () {
const cal = await getCalApi();
Cal("floatingButton", ${floatingButtonArg});
cal("floatingButton", ${floatingButtonArg});
${uiInstructionCode}
})();
}, [])
@@ -201,7 +201,7 @@ function MyComponent() {
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
return code`
import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function MyComponent() {
useEffect(()=>{
(async function () {
@@ -313,7 +313,7 @@ const embeds: {
}[] = [
{
title: "Inline Embed",
subtitle: "Loads your Cal scheduling page directly inline with your other website content",
subtitle: "Loads your event type directly inline with your other website content.",
type: "inline",
illustration: (
<svg
@@ -327,10 +327,10 @@ const embeds: {
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="2" fill="#E1E1E1" />
<rect x="48" y="86.5" width="48" height="4" rx="1" fill="#E1E1E1" />
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="6" fill="#F3F4F6" />
<rect x="48" y="86.5" width="48" height="4" rx="6" fill="#F3F4F6" />
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
@@ -378,9 +378,9 @@ const embeds: {
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="24.5" y="51" width="139" height="163" rx="1.5" stroke="#292929" />
<rect x="176" y="50.5" width="108" height="164" rx="2" fill="#E1E1E1" />
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="24.5" y="51" width="139" height="163" rx="6" stroke="#292929" />
<rect x="176" y="50.5" width="108" height="164" rx="6" fill="#F3F4F6" />
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
</svg>
),
},
@@ -400,12 +400,12 @@ const embeds: {
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="226" y="223.5" width="66" height="26" rx="2" fill="#292929" />
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
<rect x="24" y="50.5" width="120" height="76" rx="6" fill="#F3F4F6" />
<rect x="24" y="138.5" width="120" height="76" rx="6" fill="#F3F4F6" />
<rect x="156" y="50.5" width="128" height="164" rx="6" fill="#F3F4F6" />
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
<rect x="226" y="223.5" width="66" height="26" rx="6" fill="#292929" />
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
</svg>
),
@@ -426,62 +426,65 @@ const embeds: {
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" fill="#F8F8F8" />
<rect opacity="0.8" x="108" y="85" width="80" height="8" rx="2" fill="#E1E1E1" />
<rect x="108" y="97" width="48" height="4" rx="1" fill="#E1E1E1" />
<rect x="109" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="110" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="133" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="169" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="181" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="169" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="181" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="136" width="6" height="6" rx="1" fill="#3E3E3E" />
<path
d="M121 135H127V133H121V135ZM128 136V142H130V136H128ZM127 143H121V145H127V143ZM120 142V136H118V142H120ZM121 143C120.448 143 120 142.552 120 142H118C118 143.657 119.343 145 121 145V143ZM128 142C128 142.552 127.552 143 127 143V145C128.657 145 130 143.657 130 142H128ZM127 135C127.552 135 128 135.448 128 136H130C130 134.343 128.657 133 127 133V135ZM121 133C119.343 133 118 134.343 118 136H120C120 135.448 120.448 135 121 135V133Z"
fill="#3E3E3E"
/>
<rect x="133" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="169" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="181" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="169" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="181" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="169" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="181" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="172" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="169" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" stroke="#292929" />
<rect x="24" y="0.50293" width="260" height="24" rx="6" fill="#F3F4F6" />
<rect x="24" y="35" width="259" height="192" rx="5.5" fill="#F9FAFB" />
<g filter="url(#filter0_i_3223_14162)">
<rect opacity="0.8" x="40" y="99" width="24" height="24" rx="2" fill="#E5E7EB" />
<rect x="40" y="127" width="48" height="8" rx="1" fill="#E5E7EB" />
<rect x="40" y="139" width="82" height="8" rx="1" fill="#E5E7EB" />
<rect x="40" y="151" width="34" height="4" rx="1" fill="#E5E7EB" />
<rect x="40" y="159" width="34" height="4" rx="1" fill="#E5E7EB" />
</g>
<rect x="152" y="48" width="2" height="169" rx="2" fill="#E5E7EB" />
<rect opacity="0.8" x="176" y="84" width="80" height="8" rx="2" fill="#E5E7EB" />
<rect x="176" y="96" width="48" height="4" rx="1" fill="#E5E7EB" />
<rect x="177" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="109" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="201" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="237" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="249" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="237" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="249" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="135" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="187.3" y="133.4" width="9" height="9" rx="1.5" stroke="#0D121D" />
<rect x="201" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="237" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="249" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="201" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="147" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="237" y="147" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="249" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="201" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="159" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="237" y="159" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="249" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="201" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="171" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="237" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="24" y="35" width="259" height="192" rx="5.5" stroke="#101010" />
<rect x="24" y="241.503" width="260" height="24" rx="6" fill="#F3F4F6" />
</svg>
),
},
@@ -490,7 +493,7 @@ const tabs = [
{
name: "HTML",
href: "embedTabName=embed-code",
icon: FiCode,
icon: Code,
type: "code",
Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null,
@@ -506,7 +509,7 @@ const tabs = [
return (
<>
<div>
<small className="flex py-4 text-gray-500">
<small className="text-subtle flex py-4">
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
</small>
</div>
@@ -514,7 +517,7 @@ const tabs = [
data-testid="embed-code"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name="embed-code"
className="h-[calc(100%-50px)] font-mono"
className="text-default bg-default selection:bg-subtle h-[calc(100%-50px)] font-mono"
style={{ resize: "none", overflow: "auto" }}
readOnly
value={
@@ -531,7 +534,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
<!-- Cal ${embedType} embed code ends -->`
}
/>
<p className="hidden text-sm text-gray-500">
<p className="text-subtle hidden text-sm">
{t(
"Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options."
)}
@@ -543,7 +546,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
{
name: "React",
href: "embedTabName=embed-react",
icon: FiCode,
icon: Code,
type: "code",
Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null,
@@ -558,12 +561,12 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
}
return (
<>
<small className="flex py-4 text-gray-500">{t("create_update_react_component")}</small>
<small className="text-subtle flex py-4">{t("create_update_react_component")}</small>
<TextArea
data-testid="embed-react"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name="embed-react"
className="h-[calc(100%-50px)] font-mono"
className="text-default bg-default selection:bg-subtle h-[calc(100%-50px)] font-mono"
readOnly
style={{ resize: "none", overflow: "auto" }}
value={`/* First make sure that you have installed the package */
@@ -583,7 +586,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
{
name: "Preview",
href: "embedTabName=embed-preview",
icon: FiTrello,
icon: Trello,
type: "iframe",
Component: forwardRef<
HTMLIFrameElement | HTMLTextAreaElement | null,
@@ -619,7 +622,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
return (
<components.Control {...props}>
<FiSun className="ml-2 h-4 w-4 text-gray-500" />
<Sun className="text-subtle ml-2 h-4 w-4" />
{children}
</components.Control>
);
@@ -629,19 +632,19 @@ const ChooseEmbedTypesDialogContent = () => {
const { t } = useLocale();
const router = useRouter();
return (
<DialogContent type="creation" size="lg">
<div className="mb-4">
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
<div className="mb-2">
<h3 className="font-cal text-emphasis mb-2 text-2xl font-bold leading-none" id="modal-title">
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
</h3>
<div>
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
<p className="text-subtle text-sm">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
</div>
</div>
<div className="flex items-start">
<div className="items-start space-y-2 md:flex md:space-y-0">
{embeds.map((embed, index) => (
<button
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-gray-100 ltr:mr-2 rtl:ml-2"
className="hover:bg-subtle bg-muted w-full rounded-md border border-transparent p-6 text-left hover:rounded-md ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0 lg:w-1/3"
key={index}
data-testid={embed.type}
onClick={() => {
@@ -649,11 +652,11 @@ const ChooseEmbedTypesDialogContent = () => {
embedType: embed.type,
});
}}>
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
<div className="bg-default order-none box-border flex-none rounded-md border border-solid dark:bg-transparent dark:invert">
{embed.illustration}
</div>
<div className="mt-2 font-medium text-gray-900">{embed.title}</div>
<p className="text-sm text-gray-500">{embed.subtitle}</p>
<div className="text-emphasis mt-4 font-semibold">{embed.title}</div>
<p className="text-subtle mt-2 text-sm">{embed.subtitle}</p>
</button>
))}
</div>
@@ -795,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
}
const ThemeOptions = [
{ value: Theme.auto, label: "Auto Theme" },
{ value: Theme.auto, label: "Auto" },
{ value: Theme.dark, label: "Dark Theme" },
{ value: Theme.light, label: "Light Theme" },
];
@@ -812,67 +815,62 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
];
return (
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation">
<DialogContent
ref={dialogContentRef}
className="rounded-lg p-0.5 sm:max-w-[80rem]"
enableOverflow
type="creation">
<div className="flex">
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
<div className="bg-muted flex w-1/3 flex-col p-8">
<h3
className="text-emphasis mb-2.5 flex items-center text-xl font-semibold leading-5"
id="modal-title">
<button
className="h-6 w-6"
onClick={() => {
removeQueryParams(router, ["embedType", "embedTabName"]);
}}>
<FiArrowLeft className="mr-4 w-4" />
<ArrowLeft className="mr-4 w-4" />
</button>
{embed.title}
</h3>
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
<div className="flex flex-col overflow-y-auto">
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
<h4 className="text-emphasis mb-6 text-sm font-normal">{embed.subtitle}</h4>
<div className="flex flex-col">
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible
open={isEmbedCustomizationOpen}
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
<CollapsibleTrigger
type="button"
className="flex w-full items-center text-base font-medium text-gray-900">
<div>
{embedType === "inline"
? "Inline Embed Customization"
: embedType === "floating-popup"
? "Floating Popup Customization"
: "Element Click Customization"}
</div>
<FiChevronRight
className={`${
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-gray-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent className="text-sm">
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-sm">Embed Window Sizing</div>
<div className="justify-left flex items-center">
<TextField
labelProps={{ className: "hidden" }}
required
value={previewState.inline.width}
onChange={(e) => {
setPreviewState((previewState) => {
const width = e.target.value || "100%";
<div className="text-default mb-[9px] text-sm">Window sizing</div>
<div className="justify-left flex items-center !font-normal">
<div className="mr-[9px]">
<TextField
labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
required
value={previewState.inline.width}
onChange={(e) => {
setPreviewState((previewState) => {
const width = e.target.value || "100%";
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<>W</>}
/>
</div>
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<>W</>}
/>
<span className="p-2">×</span>
<TextField
labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
value={previewState.inline.height}
required
onChange={(e) => {
@@ -895,7 +893,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "" : "hidden"
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div className="mb-2 text-sm">Button Text</div>
{/* Default Values should come from preview iframe */}
@@ -919,7 +917,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div
className={classNames(
"mt-4 flex items-center justify-start",
embedType === "floating-popup" ? "space-x-2 rtl:space-x-reverse" : "hidden"
embedType === "floating-popup"
? "text-emphasis space-x-2 rtl:space-x-reverse"
: "hidden"
)}>
<Switch
defaultChecked={true}
@@ -935,12 +935,12 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
});
}}
/>
<div className="text-sm">Display Calendar Icon Button</div>
<div className="text-default text-sm">Display Calendar Icon Button</div>
</div>
<div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "" : "hidden"
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div className="mb-2">Position of Button</div>
<Select
@@ -959,7 +959,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
options={FloatingPopupPositionOptions}
/>
</div>
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div
className={classNames(
"mt-4",
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div>Button Color</div>
<div className="w-full">
<ColorPicker
@@ -980,7 +984,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
/>
</div>
</div>
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div
className={classNames(
"mt-4",
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div>Text Color</div>
<div className="w-full">
<ColorPicker
@@ -1004,22 +1012,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</CollapsibleContent>
</Collapsible>
</div>
<hr className="mt-4" />
<div className="mt-4 font-medium">
<div className="font-medium">
<Collapsible
open={isBookingCustomizationOpen}
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleTrigger className="flex w-full" type="button">
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
<FiChevronRight
className={`${
isBookingCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-gray-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-6 text-sm">
<div className="mb-4 flex items-center justify-start space-x-2 rtl:space-x-reverse">
<div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
<Switch
checked={previewState.hideEventTypeDetails}
onCheckedChange={(checked) => {
@@ -1031,15 +1030,40 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
});
}}
/>
<div className="text-sm">{t("hide_eventtype_details")}</div>
<div className="text-default text-sm">{t("hide_eventtype_details")}</div>
</div>
<Label className="">
{[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="mb-6">
<div className="mb-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
<Label>
<div className="mb-2">Theme</div>
<Select
className="w-full"
defaultValue={ThemeOptions[0]}
components={{
Control: ThemeSelectControl,
IndicatorSeparator: () => null,
}}
onChange={(option) => {
if (!option) {
@@ -1055,30 +1079,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
options={ThemeOptions}
/>
</Label>
{[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="pb-4">
<div className="mb-2 pt-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
</div>
</CollapsibleContent>
</Collapsible>
+4 -4
View File
@@ -123,7 +123,7 @@ export default function ImageUploader({
<DialogContent>
<div className="mb-4 sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<h3 className="font-cal text-lg leading-6 text-gray-900" id="modal-title">
<h3 className="font-cal text-emphasis text-lg leading-6" id="modal-title">
{t("upload_target", { target })}
</h3>
</div>
@@ -131,9 +131,9 @@ export default function ImageUploader({
<div className="mb-4">
<div className="cropper mt-6 flex flex-col items-center justify-center p-8">
{!result && (
<div className="flex h-20 max-h-20 w-20 items-center justify-start rounded-full bg-gray-50">
<div className="bg-muted flex h-20 max-h-20 w-20 items-center justify-start rounded-full">
{!imageSrc && (
<p className="w-full text-center text-sm text-white sm:text-xs">
<p className="text-inverted w-full text-center text-sm sm:text-xs">
{t("no_target", { target })}
</p>
)}
@@ -144,7 +144,7 @@ export default function ImageUploader({
</div>
)}
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
<label className="border-default bg-default hover:bg-mutedover:text-emphasis dark:hover:bg-inverted text-default dark:text-inverted focus:ring-empthasis mt-8 rounded-sm border px-3 py-1 text-xs font-medium leading-4 focus:outline-none focus:ring-2 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent">
<input
onInput={onInputFile}
type="file"
+5 -5
View File
@@ -1,5 +1,5 @@
import { AdminRequired } from "components/ui/AdminRequired";
import noop from "lodash/noop";
import { noop } from "lodash";
import type { LinkProps } from "next/link";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -69,8 +69,8 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
onClick={onClick}
className={classNames(
isCurrent
? "border-gray-900 text-gray-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
? "text-emphasis border-gray-900"
: "hover:border-default hover:text-default text-subtle border-transparent",
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
className
)}
@@ -78,7 +78,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
{tab.icon && (
<tab.icon
className={classNames(
isCurrent ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
isCurrent ? "text-emphasis" : "group-hover:text-subtle text-muted",
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
)}
aria-hidden="true"
@@ -91,7 +91,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
);
})}
</nav>
<hr />
<hr className="border-subtle" />
</>
);
};
+7 -7
View File
@@ -3,7 +3,7 @@ import React from "react";
import Shell from "@calcom/features/shell/Shell";
import { ErrorBoundary } from "@calcom/ui";
import { FiCreditCard, FiKey, FiLock, FiTerminal, FiUser, FiUsers } from "@calcom/ui/components/icon";
import { CreditCard, Key, Lock, Terminal, User, Users } from "@calcom/ui/components/icon";
import NavTabs from "./NavTabs";
@@ -11,32 +11,32 @@ const tabs = [
{
name: "profile",
href: "/settings/my-account/profile",
icon: FiUser,
icon: User,
},
{
name: "teams",
href: "/settings/teams",
icon: FiUsers,
icon: Users,
},
{
name: "security",
href: "/settings/security",
icon: FiKey,
icon: Key,
},
{
name: "developer",
href: "/settings/developer",
icon: FiTerminal,
icon: Terminal,
},
{
name: "billing",
href: "/settings/billing",
icon: FiCreditCard,
icon: CreditCard,
},
{
name: "admin",
href: "/settings/admin",
icon: FiLock,
icon: Lock,
adminRequired: true,
},
];
@@ -1,11 +1,15 @@
import type { OptionProps } from "react-select";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App";
import { Button, Select } from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon";
import {
Button,
Dropdown,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
DropdownItem,
} from "@calcom/ui";
import { Plus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -13,31 +17,6 @@ interface AdditionalCalendarSelectorProps {
isLoading?: boolean;
}
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
const { t } = useLocale();
const { data } = optionProps;
return data.slug !== "add-new" ? (
<InstallAppButton
type={data.type}
render={(installProps) => {
return (
<Button {...installProps} className="flex w-full align-top" color="minimal">
{data.image && (
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
)}
<p className="text-left">{`${t("add")} ${data.label}`}</p>
</Button>
);
}}
/>
) : (
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
<FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
<p>{t("install_new_calendar_app")}</p>
</Button>
);
};
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
@@ -59,51 +38,39 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
type: "new_other",
});
return (
<Select
name="additionalCalendar"
placeholder={
<Button StartIcon={FiPlus} color="secondary">
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
{t("add")}
</Button>
}
options={options}
isSearchable={false}
isLoading={isLoading}
components={{ Option: ImageOption }}
styles={{
menu: (defaultStyles) => ({
...defaultStyles,
"@media only screen and (max-width: 640px)": {
left: "0",
},
width: "max-content",
right: "0",
}),
indicatorSeparator: (defaultStyles) => ({
...defaultStyles,
display: "none",
}),
control: (defaultStyles) => ({
...defaultStyles,
padding: "0",
border: "0",
borderRadius: "6px",
minHeight: "auto",
}),
dropdownIndicator: (defaultStyles) => ({
...defaultStyles,
display: "none",
}),
valueContainer: (defaultStyles) => ({
...defaultStyles,
padding: "0",
}),
placeholder: (defaultStyles) => ({
...defaultStyles,
margin: "0",
}),
}}
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
{options.map((data) => (
<DropdownMenuItem key={data.slug} className="focus:outline-none">
{data.slug === "add-new" ? (
<DropdownItem StartIcon={Plus} color="minimal" href="/apps/categories/calendar">
{t("install_new_calendar_app")}
</DropdownItem>
) : (
<InstallAppButton
type={data.type}
render={(installProps) => {
const props = { ...installProps } as any;
return (
<DropdownItem {...props} color="minimal" type="button">
<span className="flex items-center gap-x-2">
{data.image && <img className="h-5 w-5" src={data.image} alt={data.label} />}
{`${t("add")} ${data.label}`}
</span>
</DropdownItem>
);
}}
/>
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</Dropdown>
);
}}
/>
+54 -48
View File
@@ -1,5 +1,6 @@
import Link from "next/link";
import { useRouter } from "next/router";
import type { IframeHTMLAttributes } from "react";
import React, { useState } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
@@ -13,16 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { App as AppType } from "@calcom/types/App";
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
import {
FiBookOpen,
FiCheck,
FiExternalLink,
FiFile,
FiFlag,
FiMail,
FiPlus,
FiShield,
} from "@calcom/ui/components/icon";
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon";
/* These app slugs all require Google Cal to be installed */
@@ -45,12 +37,12 @@ const Component = ({
tos,
privacy,
isProOnly,
images,
descriptionItems,
isTemplate,
dependencies,
}: Parameters<typeof App>[0]) => {
const { t } = useLocale();
const hasImages = images && images.length > 0;
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
const router = useRouter();
const mutation = useAddAppMutation(null, {
@@ -94,17 +86,25 @@ const Component = ({
return (
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
{hasImages && (
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
{images ? (
images.map((img) => (
<img
key={img}
src={img}
alt={`Screenshot of app ${name}`}
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
/>
))
{hasDescriptionItems && (
<div className="align-center bg-subtle mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
{descriptionItems ? (
descriptionItems.map((descriptionItem, index) =>
typeof descriptionItem === "object" ? (
<div
key={`iframe-${index}`}
className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full">
<iframe allowFullScreen {...descriptionItem.iframe} />
</div>
) : (
<img
key={descriptionItem}
src={descriptionItem}
alt={`Screenshot of app ${name}`}
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
/>
)
)
) : (
<SkeletonText />
)}
@@ -113,18 +113,22 @@ const Component = ({
<div
className={classNames(
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
hasImages && "lg:ml-8"
hasDescriptionItems && "lg:ml-8"
)}>
<div className="mb-8 flex pt-4">
<header>
<div className="mb-4 flex items-center">
<img className="min-h-16 min-w-16 h-16 w-16" src={logo} alt={name} />
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
<img
className={classNames(logo.includes("-dark") && "dark:invert", "min-h-16 min-w-16 h-16 w-16")}
src={logo}
alt={name}
/>
<h1 className="font-cal text-emphasis ml-4 text-3xl">{name}</h1>
</div>
<h2 className="text-sm font-medium text-gray-600">
<h2 className="text-default text-sm font-medium">
<Link
href={`categories/${categories[0]}`}
className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
{categories[0]}
</Link>{" "}
• {t("published_by", { author })}
@@ -140,7 +144,7 @@ const Component = ({
isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3">
<Button StartIcon={FiCheck} color="secondary" disabled>
<Button StartIcon={Check} color="secondary" disabled>
{existingCredentials.length > 0
? t("active_install", { count: existingCredentials.length })
: t("default")}
@@ -162,7 +166,7 @@ const Component = ({
}
return (
<Button
StartIcon={FiPlus}
StartIcon={Plus}
{...props}
// @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now.
@@ -234,9 +238,11 @@ const Component = ({
</span>
)}
<div className="prose-sm prose mt-8">{body}</div>
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
<span>
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
{body}
</div>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
<span className="text-default">
{price === 0 ? (
t("free_to_use_apps")
) : (
@@ -251,16 +257,16 @@ const Component = ({
)}
</span>
<h4 className="mt-8 mb-2 font-semibold text-gray-900 ">{t("learn_more")}</h4>
<h4 className="text-emphasis mt-8 mb-2 font-semibold ">{t("learn_more")}</h4>
<ul className="prose-sm -ml-1 -mr-1 leading-5">
{docs && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-sm font-normal text-black no-underline hover:underline"
className="text-emphasis text-sm font-normal no-underline hover:underline"
href={docs}>
<FiBookOpen className="mr-1 -mt-1 inline h-4 w-4 text-gray-500" />
<BookOpen className="text-subtle mr-1 -mt-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
@@ -270,9 +276,9 @@ const Component = ({
<a
target="_blank"
rel="noreferrer"
className="font-normal text-black no-underline hover:underline"
className="text-emphasis font-normal no-underline hover:underline"
href={website}>
<FiExternalLink className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
<ExternalLink className="text-subtle mr-1 -mt-px inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
@@ -282,9 +288,9 @@ const Component = ({
<a
target="_blank"
rel="noreferrer"
className="font-normal text-black no-underline hover:underline"
className="text-emphasis font-normal no-underline hover:underline"
href={"mailto:" + email}>
<FiMail className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
<Mail className="text-subtle mr-1 -mt-px inline h-4 w-4" />
{email}
</a>
@@ -295,9 +301,9 @@ const Component = ({
<a
target="_blank"
rel="noreferrer"
className="font-normal text-black no-underline hover:underline"
className="text-emphasis font-normal no-underline hover:underline"
href={tos}>
<FiFile className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
<File className="text-subtle mr-1 -mt-px inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
@@ -307,20 +313,20 @@ const Component = ({
<a
target="_blank"
rel="noreferrer"
className="font-normal text-black no-underline hover:underline"
className="text-emphasis font-normal no-underline hover:underline"
href={privacy}>
<FiShield className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
<Shield className="text-subtle mr-1 -mt-px inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
)}
</ul>
<hr className="my-8" />
<span className="leading-1 block text-xs text-gray-500">
<hr className="border-subtle my-8 border" />
<span className="leading-1 text-subtle block text-xs">
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
<FiFlag className="inline h-3 w-3" /> {t("report_app")}
<Flag className="inline h-3 w-3" /> {t("report_app")}
</a>
</div>
</div>
@@ -354,7 +360,7 @@ export default function App(props: {
privacy?: string;
licenseRequired: AppType["licenseRequired"];
isProOnly: AppType["isProOnly"];
images?: string[];
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
isTemplate?: boolean;
disableInstall?: boolean;
dependencies?: string[];
@@ -17,7 +17,7 @@ import {
Switch,
ShellSubHeading,
} from "@calcom/ui";
import { FiArrowLeft, FiCalendar, FiPlus } from "@calcom/ui/components/icon";
import { ArrowLeft, Calendar, Plus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -99,8 +99,8 @@ function CalendarSwitch(props: {
}}
/>
{!!props.destination && (
<span className="ml-8 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800 sm:ml-4">
<FiArrowLeft className="h-4 w-4" />
<span className="bg-subtle text-default ml-8 inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm font-normal sm:ml-4">
<ArrowLeft className="h-4 w-4" />
{t("adding_events_to")}
</span>
)}
@@ -177,14 +177,14 @@ function ConnectedCalendarsList(props: Props) {
credentialId={item.credentialId}
trashIcon
onSuccess={props.onChanged}
buttonProps={{ className: "border border-gray-300" }}
buttonProps={{ className: "border border-default" }}
/>
</div>
}>
<div className="border-t border-gray-200">
<div className="border-subtle border-t">
{!fromOnboarding && (
<>
<p className="px-5 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
<p className="text-subtle px-5 pt-4 text-sm">{t("toggle_calendars_conflict")}</p>
<ul className="space-y-2 px-5 py-4">
{item.calendars.map((cal) => (
<CalendarSwitch
@@ -218,7 +218,7 @@ function ConnectedCalendarsList(props: Props) {
credentialId={item.credentialId}
trashIcon
onSuccess={props.onChanged}
buttonProps={{ className: "border border-gray-300" }}
buttonProps={{ className: "border border-default" }}
/>
</div>
}
@@ -264,7 +264,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
<>
{heading && (
<div className="flex flex-col gap-6 rounded-md border p-7">
<div className="border-default flex flex-col gap-6 rounded-md border p-7">
<ShellSubHeading
title={t("calendar")}
subtitle={t("installed_app_calendar_description")}
@@ -279,18 +279,18 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
</div>
}
/>
<div className="flex justify-between rounded-md border border-gray-200 bg-gray-50 p-4">
<div className="bg-muted border-subtle flex justify-between rounded-md border p-4">
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
<div className="relative rounded-md border border-gray-200 bg-white p-1.5">
<FiCalendar className="h-8 w-8" strokeWidth="1" />
<FiPlus
className="absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2 text-black"
<div className="bg-default border-subtle relative rounded-md border p-1.5">
<Calendar className="text-default h-8 w-8" strokeWidth="1" />
<Plus
className="text-emphasis absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2"
strokeWidth="4"
/>
</div>
<div className="md:w-6/12">
<h1 className="text-sm font-semibold">{t("create_events_on")}</h1>
<p className="text-sm font-normal">{t("set_calendar")}</p>
<h1 className="text-emphasis text-sm font-semibold">{t("create_events_on")}</h1>
<p className="text-default text-sm font-normal">{t("set_calendar")}</p>
</div>
<div className="justify-end md:w-6/12">
<DestinationCalendarSelector
@@ -322,7 +322,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
</>
) : (
<EmptyScreen
Icon={FiCalendar}
Icon={Calendar}
headline={t("no_category_apps", {
category: t("calendar").toLowerCase(),
})}
@@ -1,149 +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) }),
option: (defaultStyles, state) => ({
...defaultStyles,
backgroundColor: state.isSelected
? state.isFocused
? "var(--brand-color)"
: "var(--brand-color)"
: state.isFocused
? "var(--brand-color-dark-mode)"
: "var(--brand-text-color)",
}),
control: (defaultStyles) => {
return {
...defaultStyles,
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;
@@ -6,7 +6,7 @@ import React from "react";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen } from "@calcom/ui";
import { FiAlertCircle } from "@calcom/ui/components/icon";
import { AlertCircle } from "@calcom/ui/components/icon";
type AppsLayoutProps = {
children: React.ReactNode;
@@ -27,7 +27,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
<main className="w-full">
{emptyStore ? (
<EmptyScreen
Icon={FiAlertCircle}
Icon={AlertCircle}
headline={t("no_apps")}
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
@@ -6,38 +6,38 @@ import type { InstalledAppVariants } from "@calcom/app-store/utils";
import Shell from "@calcom/features/shell/Shell";
import { trpc } from "@calcom/trpc/react";
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
import { FiBarChart, FiCalendar, FiCreditCard, FiGrid, FiShare2, FiVideo } from "@calcom/ui/components/icon";
import { BarChart, Calendar, CreditCard, Grid, Share2, Video } from "@calcom/ui/components/icon";
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{
name: "calendar",
href: "/apps/installed/calendar",
icon: FiCalendar,
icon: Calendar,
},
{
name: "conferencing",
href: "/apps/installed/conferencing",
icon: FiVideo,
icon: Video,
},
{
name: "payment",
href: "/apps/installed/payment",
icon: FiCreditCard,
icon: CreditCard,
},
{
name: "automation",
href: "/apps/installed/automation",
icon: FiShare2,
icon: Share2,
},
{
name: "analytics",
href: "/apps/installed/analytics",
icon: FiBarChart,
icon: BarChart,
},
{
name: "other",
href: "/apps/installed/other",
icon: FiGrid,
icon: Grid,
},
];
+1 -1
View File
@@ -28,7 +28,7 @@ export default function TwoFactor({ center = true }) {
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
<Label className="mt-4"> {t("2fa_code")}</Label>
<p className="mb-4 text-sm text-gray-500">{t("2fa_enabled_instructions")}</p>
<p className="text-subtle mb-4 text-sm">{t("2fa_enabled_instructions")}</p>
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
<div className="flex flex-row justify-between">
<Input
@@ -26,7 +26,7 @@ export default function AdminLayout({
const isAppsPage = router.asPath.startsWith("/settings/admin/apps");
return (
<SettingsLayout {...rest}>
<div className="mx-auto flex max-w-4xl flex-row divide-y divide-gray-200">
<div className="divide-subtle mx-auto flex max-w-4xl flex-row divide-y">
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
<ErrorBoundary>{children}</ErrorBoundary>
</div>
@@ -1,13 +1,13 @@
import React from "react";
import { Button, SkeletonText } from "@calcom/ui";
import { FiMoreHorizontal } from "@calcom/ui/components/icon";
import { MoreHorizontal } from "@calcom/ui/components/icon";
import classNames from "@lib/classNames";
function SkeletonLoader() {
return (
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<ul className="divide-subtle border-subtle bg-default animate-pulse divide-y rounded-md border sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
@@ -20,8 +20,8 @@ export default SkeletonLoader;
function SkeletonItem() {
return (
<li>
<div className="flex items-center justify-between py-5 hover:bg-neutral-50 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
<div className="items-between flex w-full flex-col justify-center hover:bg-neutral-50 sm:px-6">
<div className="flex items-center justify-between py-5 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
<div className="items-between flex w-full flex-col justify-center sm:px-6">
<SkeletonText className="my-1 h-4 w-32" />
<SkeletonText className="my-1 h-2 w-24" />
<SkeletonText className="my-1 h-2 w-40" />
@@ -31,7 +31,7 @@ function SkeletonItem() {
type="button"
variant="icon"
color="secondary"
StartIcon={FiMoreHorizontal}
StartIcon={MoreHorizontal}
disabled
/>
</div>
@@ -43,7 +43,7 @@ export const SelectSkeletonLoader = ({ className }: { className?: string }) => {
return (
<li
className={classNames(
"group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3",
"border-subtle group flex w-full items-center justify-between rounded-sm border px-[10px] py-3",
className
)}>
<div className="flex-grow truncate text-sm">
@@ -4,58 +4,65 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "@calcom/ui";
import { FiLink } from "@calcom/ui/components/icon";
import { Link } from "@calcom/ui/components/icon";
import type { Props } from "./pages/AvailabilityPage";
const excludeNullValues = (value: unknown) => !!value;
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
const { t } = useLocale();
return locations.length ? (
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
{locations.map((location, index) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
// It's possible that the location app got uninstalled
return null;
}
const renderLocations = locations.map((location, index) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
// It's possible that the location app got uninstalled
return null;
}
if (eventLocationType.variable === "hostDefault") {
return null;
}
const translateAbleKeys = [
"attendee_in_person",
"in_person",
"attendee_phone_number",
"link_meeting",
"organizer_phone_number",
];
const translateAbleKeys = [
"attendee_in_person",
"in_person",
"attendee_phone_number",
"link_meeting",
"organizer_phone_number",
];
const locationKey = z.string().default("").parse(locationKeyToString(location));
const locationKey = z.string().default("").parse(locationKeyToString(location));
const translatedLocation = location.type.startsWith("integrations:")
? eventLocationType.label
: translateAbleKeys.includes(locationKey)
? t(locationKey)
: locationKey;
const translatedLocation = location.type.startsWith("integrations:")
? eventLocationType.label
: translateAbleKeys.includes(locationKey)
? t(locationKey)
: locationKey;
return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? (
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
) : (
<img
src={eventLocationType.iconUrl}
className={classNames(
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
)}
alt={`${eventLocationType.label} icon`}
/>
return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? (
<Link className="text-default min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] " />
) : (
<img
src={eventLocationType.iconUrl}
className={classNames(
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
)}
<Tooltip content={translatedLocation}>
<p className="line-clamp-1">{translatedLocation}</p>
</Tooltip>
</div>
);
})}
alt={`${eventLocationType.label} icon`}
/>
)}
<Tooltip content={translatedLocation}>
<p className="line-clamp-1">{translatedLocation}</p>
</Tooltip>
</div>
);
});
const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[];
return filteredLocations.length ? (
<div className="text-default mr-6 flex w-full flex-col space-y-4 break-words text-sm">
{filteredLocations}
</div>
) : null;
}
+35 -10
View File
@@ -10,6 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { nameOfDay } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
@@ -24,9 +25,11 @@ type AvailableTimesProps = {
eventTypeSlug: string;
date?: Dayjs;
seatsPerTimeSlot?: number | null;
bookingAttendees?: number | null;
slots?: Slot[];
isLoading: boolean;
ethSignature?: string;
duration: number;
};
const AvailableTimes: FC<AvailableTimesProps> = ({
@@ -39,8 +42,11 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
timeFormat,
onTimeFormatChange,
seatsPerTimeSlot,
bookingAttendees,
ethSignature,
duration,
}) => {
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
const { t, i18n } = useLocale();
const router = useRouter();
@@ -61,16 +67,24 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
[isMobile]
);
const reserveSlot = (slot: Slot) => {
reserveSlotMutation.mutate({
slotUtcStartDate: slot.time,
eventTypeId,
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
});
};
return (
<div ref={slotPickerRef}>
{!!date ? (
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
<div className="mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
<div className="mb-4 flex items-center text-left text-base">
<div className="mr-4">
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
<span className="text-emphasis font-semibold">
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span>
<span className="text-bookinglight">
<span className="text-subtle">
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
</span>
</div>
@@ -92,7 +106,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
slots.map((slot) => {
type BookingURL = {
pathname: string;
query: Record<string, string | number | string[] | undefined>;
query: Record<string, string | number | string[] | undefined | TimeFormat>;
};
const bookingUrl: BookingURL = {
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
@@ -101,6 +115,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
date: dayjs.utc(slot.time).tz(timeZone()).format(),
type: eventTypeId,
slug: eventTypeSlug,
timeFormat,
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
...(ethSignature ? { ethSignature } : {}),
@@ -116,28 +131,38 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
bookingUrl.query.bookingUid = slot.bookingUid;
}
let slotFull, notEnoughSeats;
if (slot.attendees && seatsPerTimeSlot) slotFull = slot.attendees >= seatsPerTimeSlot;
if (slot.attendees && bookingAttendees && seatsPerTimeSlot)
notEnoughSeats = slot.attendees + bookingAttendees > seatsPerTimeSlot;
return (
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
{/* Current there is no way to disable Next.js Links */}
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
{seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? (
<div
className={classNames(
"text-primary-500 dark:bg-darkgray-200 dark:text-darkgray-900 mb-2 block rounded-sm border bg-white py-2 font-medium opacity-25 dark:border-transparent ",
"text-default bg-default border-subtle mb-2 block rounded-sm border py-2 font-medium opacity-25",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}>
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
{notEnoughSeats ? (
<p className="text-sm">{t("not_enough_seats")}</p>
) : slots ? (
<p className="text-sm">{t("booking_full")}</p>
) : null}
</div>
) : (
<Link
href={bookingUrl}
prefetch={false}
className={classNames(
"hover:bg-brand hover:border-brand hover:text-brandcontrast dark:hover:text-darkmodebrandcontrast",
"dark:bg-darkgray-200 dark:hover:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
" bg-default dark:bg-muted border-default hover:bg-subtle hover:border-brand-default text-emphasis mb-2 block rounded-md border py-2 text-sm font-medium",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}
onClick={() => reserveSlot(slot)}
data-testid="time">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && (
@@ -161,7 +186,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
{!isLoading && !slots.length && (
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
<h1 className="text-emphasis my-6 text-xl">{t("all_booked_today")}</h1>
</div>
)}
@@ -6,7 +6,7 @@ import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge } from "@calcom/ui";
import { FiCheckSquare, FiClock } from "@calcom/ui/components/icon";
import { CheckSquare, Clock } from "@calcom/ui/components/icon";
import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -83,32 +83,27 @@ const BookingDescription: FC<Props> = (props) => {
size="sm"
truncateAfter={3}
/>
<h2 className="mt-1 mb-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
{profile.name}
</h2>
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold leading-none text-gray-900">
<h2 className="text-default mt-1 mb-2 break-words text-sm font-medium ">{profile.name}</h2>
<h1 className="font-cal text-emphasis mb-6 break-words text-2xl font-semibold leading-none">
{eventType.title}
</h1>
<div className="dark:text-darkgray-600 flex flex-col space-y-4 text-sm font-medium text-gray-600">
<div className=" text-default flex flex-col space-y-4 text-sm font-medium">
{eventType?.description && (
<div
className={classNames(
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ",
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
isBookingPage && "text-default text-sm font-medium"
)}>
{/* TODO: Fix colors when token is introdcued to DS */}
<div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
<EventTypeDescriptionSafeHTML eventType={eventType} />
</div>
</div>
)}
{requiresConfirmation && (
<div
className={classNames(
"items-top flex",
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
)}>
<div className={classNames("items-top flex", isBookingPage && "text-default text-sm font-medium")}>
<div>
<FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<CheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
</div>
{requiresConfirmationText}
</div>
@@ -119,12 +114,12 @@ const BookingDescription: FC<Props> = (props) => {
<div
className={classNames(
"flex flex-nowrap text-sm font-medium",
isBookingPage && "dark:text-darkgray-600 text-gray-600",
isBookingPage && "text-default",
!eventType.metadata?.multipleDuration && "items-center"
)}>
<FiClock
<Clock
className={classNames(
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
isBookingPage && "mt-[2px]"
)}
/>
@@ -135,12 +130,11 @@ const BookingDescription: FC<Props> = (props) => {
<li key={idx}>
<Badge
variant="gray"
size="lg"
className={classNames(
duration === dur.toString()
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
: "hover:bg-darkgray-200 dark:hover:bg-darkmodebrand hover:text-darkgray-900 dark:hover:text-darkmodebrandcontrast dark:bg-darkgray-200 bg-gray-200 text-gray-900 dark:text-white",
"cursor-pointer"
? "bg-emphasis border-emphasis text-emphasis "
: "bg-subtle text-default border-transparent ",
"cursor-pointer border"
)}
onClick={() => {
setDuration(dur);
@@ -1,22 +1,35 @@
import type { TFunction } from "next-i18next";
import { FormattedNumber, IntlProvider } from "react-intl";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { FiCreditCard } from "@calcom/ui/components/icon";
import { CreditCard } from "@calcom/ui/components/icon";
const BookingDescriptionPayment = (props: { eventType: Parameters<typeof getPaymentAppData>[0] }) => {
const BookingDescriptionPayment = (props: {
eventType: Parameters<typeof getPaymentAppData>[0];
t: TFunction;
}) => {
const paymentAppData = getPaymentAppData(props.eventType);
if (!paymentAppData || paymentAppData.price <= 0) return null;
return (
<p className="text-bookinglight -ml-2 px-2 text-sm ">
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{paymentAppData.paymentOption === "HOLD" ? (
<>
{props.t("no_show_fee_amount", {
amount: paymentAppData.price / 100.0,
formatParams: { amount: { currency: paymentAppData.currency } },
})}
</>
) : (
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
)}
</p>
);
};
+103 -47
View File
@@ -5,6 +5,8 @@ import { useState } from "react";
import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
// TODO: Use browser locale, implement Intl in Dayjs maybe?
import "@calcom/dayjs/locales";
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
import classNames from "@calcom/lib/classNames";
import { formatTime } from "@calcom/lib/date-fns";
@@ -26,10 +28,11 @@ import {
TableActions,
TextAreaField,
} from "@calcom/ui";
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
import { Check, Clock, MapPin, RefreshCcw, Send, Slash, X, CreditCard } from "@calcom/ui/components/icon";
import useMeQuery from "@lib/hooks/useMeQuery";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
@@ -46,12 +49,17 @@ function BookingListItem(booking: BookingItemProps) {
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const user = query.data;
const { t } = useLocale();
const {
t,
i18n: { language },
} = useLocale();
const utils = trpc.useContext();
const router = useRouter();
const [rejectionReason, setRejectionReason] = useState<string>("");
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
const cardCharged = booking?.payment[0]?.success;
const mutation = trpc.viewer.bookings.confirm.useMutation({
onSuccess: (data) => {
if (data?.status === BookingStatus.REJECTED) {
@@ -94,6 +102,13 @@ function BookingListItem(booking: BookingItemProps) {
mutation.mutate(body);
};
const getSeatReferenceUid = () => {
if (!booking.seatsReferences[0]) {
return undefined;
}
return booking.seatsReferences[0].referenceUid;
};
const pendingActions: ActionType[] = [
{
id: "reject",
@@ -101,19 +116,23 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setRejectionDialogIsOpen(true);
},
icon: FiSlash,
icon: Slash,
disabled: mutation.isLoading,
},
{
id: "confirm",
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
onClick: () => {
bookingConfirm(true);
},
icon: FiCheck,
disabled: mutation.isLoading,
color: "primary",
},
// For bookings with payment, only confirm if the booking is paid for
...((isPending && !booking?.eventType?.price) || (!!booking?.eventType?.price && booking.paid)
? [
{
id: "confirm",
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
onClick: () => {
bookingConfirm(true);
},
icon: Check,
disabled: mutation.isLoading,
},
]
: []),
];
const showRecordingActions: ActionType[] = [
@@ -135,8 +154,9 @@ function BookingListItem(booking: BookingItemProps) {
cancel all remaining bookings or just that booking instance. */
href: `/booking/${booking.uid}?cancel=true${
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}`,
icon: FiX,
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
`,
icon: X,
},
{
id: "edit_booking",
@@ -144,13 +164,15 @@ function BookingListItem(booking: BookingItemProps) {
actions: [
{
id: "reschedule",
icon: FiClock,
icon: Clock,
label: t("reschedule_booking"),
href: `/reschedule/${booking.uid}`,
href: `/reschedule/${booking.uid}${
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
}`,
},
{
id: "reschedule_request",
icon: FiSend,
icon: Send,
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
label: t("send_reschedule_request"),
onClick: () => {
@@ -163,12 +185,24 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setIsOpenLocationDialog(true);
},
icon: FiMapPin,
icon: MapPin,
},
],
},
];
const chargeCardActions: ActionType[] = [
{
id: "charge_card",
label: cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee"),
disabled: cardCharged,
onClick: () => {
setChargeCardDialogIsOpen(true);
},
icon: CreditCard,
},
];
if (isTabRecurring && isRecurring) {
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
}
@@ -179,14 +213,15 @@ function BookingListItem(booking: BookingItemProps) {
const RequestSentMessage = () => {
return (
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
<FiSend className="-mt-[1px] w-4 rotate-45" />
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
</div>
<Badge startIcon={Send} size="md" variant="gray" data-testid="request_reschedule_sent">
{t("reschedule_request_sent")}
</Badge>
);
};
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
const startTime = dayjs(booking.startTime)
.locale(language)
.format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
@@ -221,10 +256,14 @@ function BookingListItem(booking: BookingItemProps) {
},
});
};
const title = booking.title;
// To be used after we run query on legacy bookings
// const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
const showRecordingsButtons =
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
const title = decodeURIComponent(booking.title);
return (
<>
<RescheduleDialog
@@ -238,6 +277,15 @@ function BookingListItem(booking: BookingItemProps) {
isOpenDialog={isOpenSetLocationDialog}
setShowLocationModal={setIsOpenLocationDialog}
/>
{booking.paid && (
<ChargeCardDialog
isOpenDialog={chargeCardDialogIsOpen}
setIsOpenDialog={setChargeCardDialogIsOpen}
bookingId={booking.id}
paymentAmount={booking?.payment[0].amount}
paymentCurrency={booking?.payment[0].currency}
/>
)}
{showRecordingsButtons && (
<ViewRecordingsDialog
booking={booking}
@@ -255,7 +303,7 @@ function BookingListItem(booking: BookingItemProps) {
label={
<>
{t("rejection_reason")}
<span className="font-normal text-gray-500"> (Optional)</span>
<span className="text-subtle font-normal"> (Optional)</span>
</>
}
value={rejectionReason}
@@ -276,13 +324,13 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent>
</Dialog>
<tr className="group flex flex-col hover:bg-gray-50 sm:flex-row">
<tr className="hover:bg-muted group flex flex-col sm:flex-row">
<td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}>
<div className="cursor-pointer py-4">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="text-sm text-gray-500">
<div className="text-emphasis text-sm leading-6">{startTime}</div>
<div className="text-subtle text-sm">
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
<MeetingTimeInTimezones
@@ -305,11 +353,11 @@ function BookingListItem(booking: BookingItemProps) {
)}
{booking.paid && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
{t("paid")}
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
</Badge>
)}
{recurringDates !== undefined && (
<div className="mt-2 text-sm text-gray-400">
<div className="text-muted mt-2 text-sm">
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
</div>
)}
@@ -319,8 +367,8 @@ function BookingListItem(booking: BookingItemProps) {
{/* Time and Badges for mobile */}
<div className="w-full pt-4 pb-2 sm:hidden">
<div className="flex w-full items-center justify-between sm:hidden">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="pr-2 text-sm text-gray-500">
<div className="text-emphasis text-sm leading-6">{startTime}</div>
<div className="text-subtle pr-2 text-sm">
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
<MeetingTimeInTimezones
@@ -349,7 +397,7 @@ function BookingListItem(booking: BookingItemProps) {
</Badge>
)}
{recurringDates !== undefined && (
<div className="text-sm text-gray-400 sm:hidden">
<div className="text-muted text-sm sm:hidden">
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
</div>
)}
@@ -359,21 +407,21 @@ function BookingListItem(booking: BookingItemProps) {
<div
title={title}
className={classNames(
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-gray-900 md:max-w-full",
"max-w-10/12 sm:max-w-56 text-emphasis text-sm font-medium leading-6 md:max-w-full",
isCancelled ? "line-through" : ""
)}>
{title}
<span> </span>
{!!booking?.eventType?.price && !booking.paid && (
<Badge className="hidden ltr:ml-2 ltr:mr-2 rtl:ml-2 sm:inline-flex" variant="orange">
<Badge className="ms-2 me-2 hidden sm:inline-flex" variant="orange">
{t("pending_payment")}
</Badge>
)}
</div>
{booking.description && (
<div
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 truncate text-sm text-gray-600"
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 text-default truncate text-sm"
title={booking.description}>
&quot;{booking.description}&quot;
</div>
@@ -386,18 +434,18 @@ function BookingListItem(booking: BookingItemProps) {
/>
)}
{isCancelled && booking.rescheduled && (
<div className="mt-2 inline-block text-left text-sm md:hidden">
<div className="mt-2 inline-block md:hidden">
<RequestSentMessage />
</div>
)}
</div>
</td>
<td className="py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:pl-0">
<td className="flex w-full justify-end py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:pl-0">
{isUpcoming && !isCancelled ? (
<>
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
{isConfirmed && <TableActions actions={bookedActions} />}
{isRejected && <div className="text-sm text-gray-500">{t("rejected")}</div>}
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
</>
) : null}
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
@@ -407,6 +455,11 @@ function BookingListItem(booking: BookingItemProps) {
<RequestSentMessage />
</div>
)}
{booking.status === "ACCEPTED" && booking.paid && booking?.payment[0]?.paymentOption === "HOLD" && (
<div className="ml-2">
<TableActions actions={chargeCardActions} />
</div>
)}
</td>
</tr>
</>
@@ -422,7 +475,10 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const user = query.data;
const { t } = useLocale();
const {
t,
i18n: { language },
} = useLocale();
const now = new Date();
const recurringCount = recurringDates.filter((date) => {
return (
@@ -452,14 +508,14 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
<p key={key} className={classNames(pastOrCancelled && "line-through")}>
{formatTime(aDate, user?.timeFormat, user?.timeZone)}
{" - "}
{dayjs(aDate).format("D MMMM YYYY")}
{dayjs(aDate).locale(language).format("D MMMM YYYY")}
</p>
);
})}>
<div className="text-gray-600 dark:text-white">
<FiRefreshCcw
<div className="text-default">
<RefreshCcw
strokeWidth="3"
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
className="text-muted float-left mr-1 mt-1.5 inline-block h-3 w-3"
/>
<p className="mt-1 pl-5 text-xs">
{booking.status === BookingStatus.ACCEPTED
@@ -532,13 +588,13 @@ const DisplayAttendees = ({
}) => {
const { t } = useLocale();
return (
<div className="text-sm text-gray-900">
<div className="text-emphasis text-sm">
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
{attendees.length > 1 ? <span>,&nbsp;</span> : <span>&nbsp;{t("and")}&nbsp;</span>}
<Attendee {...attendees[0]} />
{attendees.length > 1 && (
<>
<div className="inline-block text-sm text-gray-900">&nbsp;{t("and")}&nbsp;</div>
<div className="text-emphasis inline-block text-sm">&nbsp;{t("and")}&nbsp;</div>
{attendees.length > 2 ? (
<Tooltip
content={attendees.slice(1).map((attendee) => (
+24 -18
View File
@@ -1,12 +1,11 @@
import { useRouter } from "next/router";
import { useState } from "react";
import { useCallback, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Button, TextArea } from "@calcom/ui";
import { FiX } from "@calcom/ui/components/icon";
import { X } from "@calcom/ui/components/icon";
type Props = {
booking: {
@@ -23,27 +22,34 @@ type Props = {
setIsCancellationMode: (value: boolean) => void;
theme: string | null;
allRemainingBookings: boolean;
seatReferenceUid?: string;
};
export default function CancelBooking(props: Props) {
const [cancellationReason, setCancellationReason] = useState<string>("");
const { t } = useLocale();
const router = useRouter();
const { booking, allRemainingBookings } = props;
const { booking, allRemainingBookings, seatReferenceUid } = props;
const [loading, setLoading] = useState(false);
const telemetry = useTelemetry();
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
useTheme(props.theme);
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
if (node !== null) {
node.scrollIntoView({ behavior: "smooth" });
node.focus();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
{error && (
<div className="mt-8">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<FiX className="h-6 w-6 text-red-600" />
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<X className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
{error}
</h3>
</div>
@@ -51,12 +57,13 @@ export default function CancelBooking(props: Props) {
)}
{!error && (
<div className="mt-5 sm:mt-6">
<label className="text-bookingdark font-medium dark:text-white">{t("cancellation_reason")}</label>
<label className="text-default font-medium">{t("cancellation_reason")}</label>
<TextArea
ref={cancelBookingRef}
placeholder={t("cancellation_reason_placeholder")}
value={cancellationReason}
onChange={(e) => setCancellationReason(e.target.value)}
className="dark:bg-darkgray-100 dark:border-darkgray-400 mt-2 mb-4 w-full dark:text-white "
className="mt-2 mb-4 w-full "
rows={3}
/>
<div className="flex flex-col-reverse rtl:space-x-reverse ">
@@ -68,21 +75,20 @@ export default function CancelBooking(props: Props) {
{t("nevermind")}
</Button>
<Button
className="flex justify-center"
data-testid="cancel"
onClick={async () => {
setLoading(true);
const payload = {
id: booking?.id,
cancellationReason: cancellationReason,
allRemainingBookings,
};
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
const res = await fetch("/api/cancel", {
body: JSON.stringify(payload),
body: JSON.stringify({
uid: booking?.uid,
cancellationReason: cancellationReason,
allRemainingBookings,
// @NOTE: very important this shouldn't cancel with number ID use uid instead
seatReferenceUid,
}),
headers: {
"Content-Type": "application/json",
},
@@ -4,7 +4,7 @@ import { SkeletonText } from "@calcom/ui";
function SkeletonLoader() {
return (
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
<ul className="divide-subtle border-subtle bg-default animate-pulse divide-y rounded-md border sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
+6 -5
View File
@@ -78,6 +78,7 @@ export const SlotPicker = ({
recurringEventCount,
users,
seatsPerTimeSlot,
bookingAttendees,
weekStart = 0,
ethSignature,
}: {
@@ -89,6 +90,7 @@ export const SlotPicker = ({
onTimeFormatChange: (is24Hour: boolean) => void;
timeZone?: string;
seatsPerTimeSlot?: number;
bookingAttendees?: number;
recurringEventCount?: number;
users: string[];
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
@@ -131,10 +133,7 @@ export const SlotPicker = ({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
startTime:
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
: browsingDate?.startOf("month"),
startTime: dayjs().startOf("day"),
endTime: browsingDate?.endOf("month"),
timeZone,
duration,
@@ -165,7 +164,7 @@ export const SlotPicker = ({
isLoading={isLoading}
className={classNames(
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]",
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
selectedDate ? " border-subtle sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
)}
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
locale={isLocaleReady ? i18n.language : "en"}
@@ -192,8 +191,10 @@ export const SlotPicker = ({
eventTypeId={eventType.id}
eventTypeSlug={eventType.slug}
seatsPerTimeSlot={seatsPerTimeSlot}
bookingAttendees={bookingAttendees}
recurringCount={recurringEventCount}
ethSignature={ethSignature}
duration={parseInt(duration)}
/>
</>
);
+6 -53
View File
@@ -1,12 +1,9 @@
import type { FC } from "react";
import { useEffect, useState } from "react";
import useTheme from "@calcom/lib/hooks/useTheme";
import type { ITimezoneOption } from "@calcom/ui";
import { TimezoneSelect } from "@calcom/ui";
import useMeQuery from "@lib/hooks/useMeQuery";
import { timeZone } from "../../lib/clock";
type Props = {
@@ -15,8 +12,6 @@ type Props = {
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
const [selectedTimeZone, setSelectedTimeZone] = useState("");
const query = useMeQuery();
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
useEffect(() => {
setSelectedTimeZone(timeZone());
@@ -28,59 +23,17 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
}
}, [selectedTimeZone, onSelectTimeZone]);
const customStyles = {
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
padding: "10px 12px !important",
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
...(userTheme === "dark" && {
color: state.isSelected || state.isFocused ? "white" : "#80868B",
}),
}),
control: () => ({
display: "flex",
cursor: "pointer",
backgroundColor: "transparent !important",
minWidth: "5rem",
height: "24px",
minHeight: "24px !important",
boxShadow: "none !important",
}),
singleValue: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
...provided,
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
...(userTheme === "dark" && {
color: "#a5a5a5 !important",
}),
}),
menu: (provided: object) => ({
...provided,
minWidth: "20rem",
margin: "8px 0 0 -20px",
}),
valueContainer: (provided: object) => ({
...provided,
padding: "0 0 0 6px",
}),
dropdownIndicator: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
...provided,
transform: state.selectProps.menuIsOpen ? "rotate(180deg)" : "",
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
marginLeft: 4,
padding: 0,
...(userTheme === "dark" && {
color: "#80868B",
}),
}),
};
return !!selectedTimeZone ? (
<TimezoneSelect
id="timeZone"
classNames={{
singleValue: () => "text-default",
dropdownIndicator: () => "text-default",
menu: () => "!w-64 max-w-[90vw]",
}}
variant="minimal"
value={selectedTimeZone}
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
className="flex h-6 text-sm font-medium"
styles={customStyles}
/>
) : null;
};
@@ -1,4 +1,4 @@
import { FiGlobe } from "@calcom/ui/components/icon";
import { Globe } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
@@ -17,8 +17,8 @@ export function TimezoneDropdown({
return (
<>
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white">
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
<Globe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</div>
</>
@@ -14,17 +14,17 @@ import {
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import notEmpty from "@calcom/lib/notEmpty";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import { HeadSeo } from "@calcom/ui";
import { FiCreditCard, FiRefreshCcw } from "@calcom/ui/components/icon";
import { trpc } from "@calcom/trpc";
import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
@@ -52,16 +52,29 @@ const dateQuerySchema = z.object({
rescheduleUid: z.string().optional().default(""),
date: z.string().optional().default(""),
timeZone: z.string().optional().default(""),
seatReferenceUid: z.string().optional(),
});
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor: string; darkBrandColor: string }) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
};
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
const router = useRouter();
const isEmbed = useIsEmbed(restProps.isEmbed);
const query = dateQuerySchema.parse(router.query);
const { rescheduleUid } = query;
useTheme(profile.theme);
useBrandColors({
brandColor: profile.brandColor,
darkBrandColor: profile.darkBrandColor,
});
const { t } = useLocale();
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
@@ -91,8 +104,9 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
/*
const telemetry = useTelemetry();
useEffect(() => {
useEffect(() => {
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
@@ -100,7 +114,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
);
}
}, [telemetry]);
}, [telemetry]); */
const embedUiConfig = useEmbedUiConfig();
// get dynamic user list here
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
@@ -110,6 +124,16 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
[timeZone]
);
const paymentAppData = getPaymentAppData(eventType);
const paymentAmount = () => {
return;
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>;
};
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
const rawSlug = profile.slug ? profile.slug.split("/") : [];
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
@@ -124,6 +148,15 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
: undefined,
];
const { data: bookingAttendees } = trpc.viewer.bookings.getBookingAttendees.useQuery(
{
seatReferenceUid: rescheduleUid,
},
{
enabled: !!(rescheduleUid && eventType.seatsPerTimeSlot),
}
);
return (
<Gates gates={gates} appData={rainbowAppData} dispatch={gateDispatcher}>
<HeadSeo
@@ -143,52 +176,68 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
nofollow: eventType.hidden,
noindex: eventType.hidden,
}}
isBrandingHidden={restProps.isBrandingHidden}
/>
<BookingPageTagManager eventType={eventType} />
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<div>
<main
className={classNames(
"flex flex-col md:mx-4",
shouldAlignCentrally ? "items-center" : "items-start",
!isEmbed && classNames("mx-auto my-0 ease-in-out md:my-24")
!isEmbed && classNames("bg-subtle dark:bg-default mx-auto my-0 ease-in-out md:my-24")
)}>
<div>
<div
style={availabilityDatePickerEmbedStyles}
className={classNames(
isBackgroundTransparent
? ""
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
"border-bookinglightest md:rounded-md md:border",
isBackgroundTransparent ? "" : "bg-default dark:bg-muted pb-4 md:pb-0",
"border-booker md:border-booker-width md:rounded-md",
isEmbed && "mx-auto"
)}>
<div className="md:flex">
{showEventTypeDetails && (
<div
className={classNames(
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
"min-w-full md:w-[280px] md:min-w-[280px]",
" border-subtle flex flex-col p-5 sm:border-r",
"min-w-full md:w-[230px] md:min-w-[230px]",
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
)}>
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
{rescheduleUid && eventType.seatsPerTimeSlot && bookingAttendees && (
<div
className={classNames(
"flex flex-nowrap items-center text-sm font-medium",
" text-default",
"ltr:mr-[10px] rtl:ml-[10px]"
)}>
<User
className={classNames(
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
"mt-[2px]"
)}
/>{" "}
{t("event_type_seats", { numberOfSeats: bookingAttendees })}
</div>
)}
{!rescheduleUid && eventType.recurringEvent && (
<div className="flex items-start text-sm font-medium">
<FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<RefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<div>
<p className="mb-1 -ml-2 inline px-2 py-1">
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
</p>
<input
type="number"
<NumberInput
defaultValue={eventType.recurringEvent.count}
min="1"
max={eventType.recurringEvent.count}
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
defaultValue={eventType.recurringEvent.count}
isFullWidth={false}
className="me-2 inline w-16"
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
}}
/>
<p className="inline">
{t("occurrence", {
count: recurringEventCount,
@@ -199,14 +248,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
)}
{paymentAppData.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium">
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{paymentAppData.paymentOption === "HOLD" ? (
<>
{t("no_show_fee_amount", {
amount: paymentAppData.price / 100.0,
formatParams: { amount: { currency: paymentAppData.currency } },
})}
</>
) : (
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
)}
</p>
)}
{timezoneDropdown}
@@ -215,12 +273,12 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
<div>
<p
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
className="mt-4 mb-3 text-default"
data-testid="former_time_p_desktop">
{t("former_time")}
</p>
<p className="text-gray-500 line-through dark:text-darkgray-600">
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
<p className="text-subtle line-through ">
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-subtle" />
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
</p>
</div>
@@ -247,6 +305,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
timeZone={timeZone}
users={userList}
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
bookingAttendees={bookingAttendees || undefined}
recurringEventCount={recurringEventCount}
ethSignature={gateState.rainbowToken}
/>
+165 -122
View File
@@ -4,18 +4,16 @@ import { useSession } from "next-auth/react";
import dynamic from "next/dynamic";
import Head from "next/head";
import { useRouter } from "next/router";
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
import { useEffect, useMemo, useReducer, useState } from "react";
import { useForm, useFormContext } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import { getEventTypeAppData } from "@calcom/app-store/utils";
import type { LocationObject } from "@calcom/core/location";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
@@ -30,25 +28,28 @@ import {
import getBookingResponsesSchema, {
getBookingResponsesPartialSchema,
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
import CustomBranding from "@calcom/lib/CustomBranding";
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import classNames from "@calcom/lib/classNames";
import { APP_NAME } from "@calcom/lib/constants";
import { APP_NAME, MINUTES_TO_BOOK } from "@calcom/lib/constants";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Button, Form, Tooltip } from "@calcom/ui";
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc";
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/icon";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import createBooking from "@lib/mutations/bookings/create-booking";
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
import { parseDate, parseRecurringDates } from "@lib/parseDate";
import { parseRecurringDates, parseDate } from "@lib/parseDate";
import type { Gate, GateState } from "@components/Gates";
import Gates from "@components/Gates";
@@ -58,11 +59,21 @@ import type { BookPageProps } from "../../../pages/[user]/book";
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
/** These are like 40kb that not every user needs */
const BookingDescriptionPayment = dynamic(
() => import("@components/booking/BookingDescriptionPayment")
) as unknown as typeof import("@components/booking/BookingDescriptionPayment").default;
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor?: string; darkBrandColor?: string }) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
};
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
const BookingFields = ({
fields,
@@ -78,6 +89,7 @@ const BookingFields = ({
const { t } = useLocale();
const { watch, setValue } = useFormContext();
const locationResponse = watch("responses.location");
const currentView = rescheduleUid ? "reschedule" : "";
return (
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
@@ -87,12 +99,16 @@ const BookingFields = ({
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
let readOnly =
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
let noLabel = false;
let hidden = !!field.hidden;
const fieldViews = field.views;
if (fieldViews && !fieldViews.find((view) => view.id === currentView)) {
return null;
}
if (field.name === SystemField.Enum.rescheduleReason) {
if (!rescheduleUid) {
return null;
}
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
readOnly = false;
}
@@ -115,7 +131,10 @@ const BookingFields = ({
}
// We don't show `notes` field during reschedule
if (field.name === SystemField.Enum.notes && !!rescheduleUid) {
if (
(field.name === SystemField.Enum.notes || field.name === SystemField.Enum.guests) &&
!!rescheduleUid
) {
return null;
}
@@ -126,26 +145,14 @@ const BookingFields = ({
}
const optionsInputs = field.optionsInputs;
const options = locations.map((location) => {
const eventLocation = getEventLocationType(location.type);
const locationString = locationKeyToString(location);
if (typeof locationString !== "string" || !eventLocation) {
// It's possible that location app got uninstalled
return null;
}
const type = eventLocation.type;
const optionInput = optionsInputs[type as keyof typeof optionsInputs];
// TODO: Instead of `getLocationOptionsForSelect` options should be retrieved from dataStore[field.getOptionsAt]. It would make it agnostic of the `name` of the field.
const options = getLocationOptionsForSelect(locations, t);
options.forEach((option) => {
const optionInput = optionsInputs[option.value as keyof typeof optionsInputs];
if (optionInput) {
optionInput.placeholder = t(eventLocation?.attendeeInputPlaceholder || "");
optionInput.placeholder = option.inputPlaceholder;
}
return {
label: t(locationString),
value: type,
};
});
field.options = options.filter(
(location): location is NonNullable<(typeof options)[number]> => !!location
);
@@ -177,9 +184,26 @@ const BookingFields = ({
);
};
const routerQuerySchema = z
.object({
timeFormat: z.nativeEnum(TimeFormat),
rescheduleUid: z.string().optional(),
date: z
.string()
.optional()
.transform((date) => {
if (date === undefined) {
return null;
}
return date;
}),
})
.passthrough();
const BookingPage = ({
eventType,
booking,
currentSlotBooking,
profile,
isDynamicGroupBooking,
recurringEventCount,
@@ -187,8 +211,11 @@ const BookingPage = ({
hashedLink,
...restProps
}: BookingPageProps) => {
const removeSelectedSlotMarkMutation = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation();
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const { t, i18n } = useLocale();
const { duration: queryDuration } = useRouterQuery("duration");
const { date: queryDate } = useRouterQuery("date");
const isEmbed = useIsEmbed(restProps.isEmbed);
const embedUiConfig = useEmbedUiConfig();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
@@ -204,6 +231,15 @@ const BookingPage = ({
}),
{}
);
const reserveSlot = () => {
if (queryDuration) {
reserveSlotMutation.mutate({
eventTypeId: eventType.id,
slotUtcStartDate: dayjs(queryDate).utc().format(),
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
});
}
};
// Define duration now that we support multiple duration eventTypes
let duration = eventType.length;
if (
@@ -215,39 +251,31 @@ const BookingPage = ({
duration = Number(queryDuration);
}
// This is a workaround for forcing the same time format for both server side rendering and client side rendering
// At initial render, we use the default time format which is 12H
const [withDefaultTimeFormat, setWithDefaultTimeFormat] = useState(true);
const parseDateFunc = useCallback(
(date: string | null | Dayjs) => {
return parseDate(date, i18n, withDefaultTimeFormat);
},
[withDefaultTimeFormat]
);
// After intial render on client side, we let parseDateFunc to use the time format from the localStorage
useEffect(() => {
setWithDefaultTimeFormat(false);
}, []);
useEffect(() => {
if (top !== window) {
/* if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
telemetryEventTypes.embedView,
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
);
}
} */
reserveSlot();
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
return () => {
clearInterval(interval);
removeSelectedSlotMarkMutation.mutate();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const mutation = useMutation(createBooking, {
onSuccess: async (responseData) => {
const { uid, paymentUid } = responseData;
const { uid } = responseData;
if (paymentUid) {
if ("paymentUid" in responseData && !!responseData.paymentUid) {
return await router.push(
createPaymentLink({
paymentUid,
paymentUid: responseData.paymentUid,
date,
name: bookingForm.getValues("responses.name"),
email: bookingForm.getValues("responses.email"),
@@ -256,14 +284,19 @@ const BookingPage = ({
);
}
return router.push({
pathname: `/booking/${uid}`,
query: {
isSuccessBookingPage: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
},
const query = {
isSuccessBookingPage: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
};
return bookingSuccessRedirect({
router,
successRedirectUrl: eventType.successRedirectUrl,
query,
bookingUid: uid,
});
},
});
@@ -271,24 +304,37 @@ const BookingPage = ({
const recurringMutation = useMutation(createRecurringBooking, {
onSuccess: async (responseData = []) => {
const { uid } = responseData[0] || {};
return router.push({
pathname: `/booking/${uid}`,
query: {
allRemainingBookings: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
formerTime: booking?.startTime.toString(),
},
const query = {
isSuccessBookingPage: true,
allRemainingBookings: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
formerTime: booking?.startTime.toString(),
};
return bookingSuccessRedirect({
router,
successRedirectUrl: eventType.successRedirectUrl,
query,
bookingUid: uid,
});
},
});
const rescheduleUid = router.query.rescheduleUid as string;
const {
data: { timeFormat, rescheduleUid, date },
} = useTypedQuery(routerQuerySchema);
useTheme(profile.theme);
const date = asStringOrNull(router.query.date);
useBrandColors({
brandColor: profile.brandColor,
darkBrandColor: profile.darkBrandColor,
});
const querySchema = getBookingResponsesPartialSchema({
bookingFields: getBookingFieldsWithSystemFields(eventType),
eventType: {
bookingFields: getBookingFieldsWithSystemFields(eventType),
},
view: rescheduleUid ? "reschedule" : "booking",
});
const parsedQuery = querySchema.parse({
@@ -368,7 +414,8 @@ const BookingPage = ({
const bookingFormSchema = z
.object({
responses: getBookingResponsesSchema({
bookingFields: getBookingFieldsWithSystemFields(eventType),
eventType: { bookingFields: getBookingFieldsWithSystemFields(eventType) },
view: rescheduleUid ? "reschedule" : "booking",
}),
})
.passthrough();
@@ -386,30 +433,21 @@ const BookingPage = ({
// Calculate the booking date(s)
let recurringStrings: string[] = [],
recurringDates: Date[] = [];
const parseRecurringDatesFunc = useCallback(
(date: string | null | Dayjs, recurringEvent: RecurringEvent, recurringCount: number) => {
return parseRecurringDates(
{
startDate: date,
timeZone: timeZone(),
recurringEvent: recurringEvent,
recurringCount: recurringCount,
withDefaultTimeFormat: withDefaultTimeFormat,
},
i18n
);
},
[withDefaultTimeFormat, date, eventType.recurringEvent, recurringEventCount]
);
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
[recurringStrings, recurringDates] = parseRecurringDatesFunc(
date,
eventType.recurringEvent,
parseInt(recurringEventCount.toString())
[recurringStrings, recurringDates] = parseRecurringDates(
{
startDate: date,
timeZone: timeZone(),
recurringEvent: eventType.recurringEvent,
recurringCount: parseInt(recurringEventCount.toString()),
selectedTimeFormat: timeFormat,
},
i18n
);
}
const bookEvent = (booking: BookingFormValues) => {
const bookEvent = (bookingValues: BookingFormValues) => {
telemetry.event(
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
{ isTeamBooking: document.URL.includes("team/") }
@@ -432,9 +470,9 @@ const BookingPage = ({
// Identify set of bookings to one intance of recurring event to support batch changes
const recurringEventId = uuidv4();
const recurringBookings = recurringDates.map((recurringDate) => ({
...booking,
start: dayjs(recurringDate).format(),
end: dayjs(recurringDate).add(duration, "minute").format(),
...bookingValues,
start: dayjs(recurringDate).utc().format(),
end: dayjs(recurringDate).utc().add(duration, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
recurringEventId,
@@ -452,20 +490,21 @@ const BookingPage = ({
recurringMutation.mutate(recurringBookings);
} else {
mutation.mutate({
...booking,
start: dayjs(date).tz(timeZone()).format(),
end: dayjs(date).tz(timeZone()).add(duration, "minute").format(),
...bookingValues,
start: dayjs(date).utc().format(),
end: dayjs(date).utc().add(duration, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
timeZone: timeZone(),
language: i18n.language,
rescheduleUid,
bookingUid: router.query.bookingUid as string,
bookingUid: (router.query.bookingUid as string) || booking?.uid,
user: router.query.user,
metadata,
hasHashedBookingLink,
hashedLink,
ethSignature: gateState.rainbowToken,
seatReferenceUid: router.query.seatReferenceUid as string,
});
}
};
@@ -499,7 +538,6 @@ const BookingPage = ({
<link rel="icon" href="/favico.ico" />
</Head>
<BookingPageTagManager eventType={eventType} />
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
@@ -509,17 +547,17 @@ const BookingPage = ({
<div
className={classNames(
"main",
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border",
"dark:border-darkgray-300 rounded-md sm:border"
isBackgroundTransparent ? "" : "bg-default dark:bg-muted",
"border-booker sm:border-booker-width rounded-md"
)}>
<div className="sm:flex">
{showEventTypeDetails && (
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
<div className="sm:border-subtle text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
<BookingDescriptionPayment eventType={eventType} />
<BookingDescriptionPayment eventType={eventType} t={t} />
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
<FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<div className="dark:text-inverted text-default items-start text-sm font-medium">
<RefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<p className="-ml-2 inline-block items-center px-2">
{getEveryFreqFor({
t,
@@ -530,11 +568,11 @@ const BookingPage = ({
</div>
)}
<div className="text-bookinghighlight flex items-start text-sm">
<FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<Calendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<div className="text-sm font-medium">
{isClientTimezoneAvailable &&
(rescheduleUid || !eventType.recurringEvent?.freq) &&
`${parseDateFunc(date)}`}
`${parseDate(date, i18n, timeFormat)}`}
{isClientTimezoneAvailable &&
!rescheduleUid &&
eventType.recurringEvent?.freq &&
@@ -547,7 +585,7 @@ const BookingPage = ({
content={recurringStrings.slice(5).map((timeFormatted, key) => (
<p key={key}>{timeFormatted}</p>
))}>
<p className="dark:text-darkgray-600 text-sm">
<p className=" text-sm">
+ {t("plus_more", { count: recurringStrings.length - 5 })}
</p>
</Tooltip>
@@ -561,34 +599,38 @@ const BookingPage = ({
{t("former_time")}
</p>
<p className="line-through ">
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<Calendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{isClientTimezoneAvailable &&
typeof booking.startTime === "string" &&
parseDateFunc(dayjs(booking.startTime))}
parseDate(dayjs(booking.startTime), i18n, timeFormat)}
</p>
</div>
)}
{!!eventType.seatsPerTimeSlot && (
<div className="text-bookinghighlight flex items-start text-sm">
<FiUser
<User
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600"
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
: currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-bookinghighlight"
}`}
/>
<p
className={`${
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600"
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
: currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-bookinghighlight"
} mb-2 font-medium`}>
{booking
? eventType.seatsPerTimeSlot - booking.attendees.length
{currentSlotBooking
? eventType.seatsPerTimeSlot - currentSlotBooking.attendees.length
: eventType.seatsPerTimeSlot}{" "}
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
</p>
@@ -626,13 +668,14 @@ const BookingPage = ({
</Button>
</div>
</Form>
{(mutation.isError || recurringMutation.isError) && (
{mutation.isError || recurringMutation.isError ? (
<ErrorMessage error={mutation.error || recurringMutation.error} />
)}
) : null}
</div>
</div>
</div>
</main>
<Toaster position="bottom-right" />
</Gates>
);
};
@@ -648,9 +691,9 @@ function ErrorMessage({ error }: { error: unknown }) {
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<FiAlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
<AlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
</div>
<div className="ltr:ml-3 rtl:mr-3">
<div className="ms-3">
<p className="text-sm text-blue-700">
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
{error instanceof HttpError || error instanceof Error ? (
@@ -0,0 +1,100 @@
import { Trans } from "next-i18next";
import { useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import { IntlProvider, FormattedNumber } from "react-intl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
showToast,
} from "@calcom/ui";
import { CreditCard, AlertTriangle } from "@calcom/ui/components/icon";
interface IRescheduleDialog {
isOpenDialog: boolean;
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
bookingId: number;
paymentAmount: number;
paymentCurrency: string;
}
export const ChargeCardDialog = (props: IRescheduleDialog) => {
const { t } = useLocale();
const utils = trpc.useContext();
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
const [chargeError, setChargeError] = useState(false);
const chargeCardMutation = trpc.viewer.payments.chargeCard.useMutation({
onSuccess: () => {
utils.viewer.bookings.invalidate();
setIsOpenDialog(false);
showToast("Charge successful", "success");
},
onError: () => {
setChargeError(true);
},
});
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<CreditCard className="m-auto h-6 w-6" />
</div>
<div className="pt-1">
<DialogHeader title={t("charge_card")} />
<Trans i18nKey="charge_card_dialog_body">
<p className="text-sm text-gray-500">
You are about to charge the attendee{" "}
<IntlProvider locale="en">
<FormattedNumber
value={props.paymentAmount / 100.0}
style="currency"
currency={props.paymentCurrency?.toUpperCase()}
/>
</IntlProvider>
. Are you sure you want to continue?
</p>
</Trans>
{chargeError && (
<div className="mt-4 flex text-red-500">
<AlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
<p className="text-sm">{t("error_charging_card")}</p>
</div>
)}
<DialogFooter>
<DialogClose />
<Button
data-testid="send_request"
disabled={chargeCardMutation.isLoading || chargeError}
onClick={() =>
chargeCardMutation.mutate({
bookingId,
})
}>
<Trans i18nKey="charge_card_confirm">
Charge attendee{" "}
<IntlProvider locale="en">
<FormattedNumber
value={props.paymentAmount / 100.0}
style="currency"
currency={props.paymentCurrency?.toUpperCase()}
/>
</IntlProvider>
</Trans>
</Button>
</DialogFooter>
</div>
</div>
</DialogContent>
</Dialog>
);
};
@@ -17,8 +17,9 @@ import {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Input } from "@calcom/ui";
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
import { FiMapPin } from "@calcom/ui/components/icon";
import { MapPin } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -32,6 +33,7 @@ interface ISetLocationDialog {
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
selection?: LocationOption;
booking?: BookingItem;
isTeamEvent?: boolean;
defaultValues?: LocationObject[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean;
@@ -52,7 +54,7 @@ const LocationInput = (props: {
const { control } = useFormContext() as typeof locationFormMethods;
if (eventLocationType?.organizerInputType === "text") {
return (
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
<Input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
);
} else if (eventLocationType?.organizerInputType === "phone") {
return (
@@ -73,6 +75,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
saveLocation,
selection,
booking,
isTeamEvent,
setShowLocationModal,
isOpenDialog,
defaultValues,
@@ -161,7 +164,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
return (
<div>
<label htmlFor="locationInput" className="block text-sm font-medium text-gray-700">
<label htmlFor="locationInput" className="text-default block text-sm font-medium">
{t(eventLocationType.messageForOrganizer || "")}
</label>
<div className="mt-1">
@@ -171,7 +174,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
id="locationInput"
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
required
className="block w-full rounded-sm border-gray-300 text-sm"
className="border-default block w-full rounded-sm text-sm"
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
defaultValue={
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
@@ -180,7 +183,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
<ErrorMessage
errors={locationFormMethods.formState.errors}
name={eventLocationType.variable}
className="mt-1 text-sm text-red-500"
className="text-error mt-1 text-sm"
as="p"
/>
</div>
@@ -206,7 +209,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
</div>
);
} else {
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
return <p className="text-default text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
}
})();
@@ -214,16 +217,16 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
<FiMapPin className="text-primary-600 h-6 w-6" />
<div className="bg-subtle mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
<MapPin className="text-emphasis h-6 w-6" />
</div>
<div className="w-full">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
{t("edit_location")}
</h3>
{!booking && (
<p className="text-sm text-gray-400">
<p className="text-default text-sm">
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
Can&apos;t find the right video app? Visit our
<Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video">
@@ -238,8 +241,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
{booking && (
<>
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
<p className="mb-2 ml-1 text-sm text-black">
<p className="text-emphasis mt-6 mb-2 ml-1 text-sm font-bold">{t("current_location")}:</p>
<p className="text-emphasis mb-2 ml-1 text-sm">
{getHumanReadableLocationValue(booking.location, t)}
</p>
</>
@@ -290,7 +293,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
query={locationsQuery}
success={({ data }) => {
if (!data.length) return null;
const locationOptions = [...data];
const locationOptions = [...data].filter((option) => {
return !isTeamEvent ? option.label !== "Conferencing" : true;
});
if (booking) {
locationOptions.map((location) =>
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
@@ -13,7 +13,7 @@ import {
showToast,
TextArea,
} from "@calcom/ui";
import { FiClock } from "@calcom/ui/components/icon";
import { Clock } from "@calcom/ui/components/icon";
interface IRescheduleDialog {
isOpenDialog: boolean;
@@ -44,14 +44,14 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<FiClock className="m-auto h-6 w-6" />
<Clock className="m-auto h-6 w-6" />
</div>
<div className="pt-1">
<DialogHeader title={t("send_reschedule_request")} />
<p className="text-sm text-gray-500">{t("reschedule_modal_description")}</p>
<p className="mt-6 mb-2 text-sm font-bold text-black">
<p className="text-subtle text-sm">{t("reschedule_modal_description")}</p>
<p className="text-emphasis mt-6 mb-2 text-sm font-bold">
{t("reason_for_reschedule_request")}
<span className="font-normal text-gray-500"> (Optional)</span>
<span className="text-subtle font-normal"> (Optional)</span>
</p>
<TextArea
data-testid="reschedule_reason"
+8 -8
View File
@@ -29,15 +29,15 @@ const ErrorDebugPanel: React.FC<{ error: Props["error"]; children?: never }> = (
];
return (
<div className="overflow-hidden bg-white shadow sm:rounded-lg">
<div className="border-t border-gray-200 px-4 py-5 sm:p-0">
<dl className="sm:divide-y sm:divide-gray-200">
<div className="bg-default overflow-hidden shadow sm:rounded-lg">
<div className="border-subtle border-t px-4 py-5 sm:p-0">
<dl className="sm:divide-subtle sm:divide-y">
{debugMap.map(([key, value]) => {
if (value !== undefined) {
return (
<div key={key} className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
<dt className="text-sm font-bold text-black">{key}</dt>
<dd className="mt-1 text-sm text-black sm:col-span-2 sm:mt-0">{value}</dd>
<dt className="text-emphasis text-sm font-bold">{key}</dt>
<dd className="text-emphasis mt-1 text-sm sm:col-span-2 sm:mt-0">{value}</dd>
</div>
);
}
@@ -53,11 +53,11 @@ export const ErrorPage: React.FC<Props> = (props) => {
return (
<>
<div className="min-h-screen bg-white px-4">
<div className="bg-default min-h-screen px-4">
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
<div className="text-center">
<p className="text-sm font-semibold uppercase tracking-wide text-black">{statusCode}</p>
<h1 className="mt-2 text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl">
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">{statusCode}</p>
<h1 className="text-emphasis mt-2 text-4xl font-extrabold tracking-tight sm:text-5xl">
{message}
</h1>
</div>
@@ -58,35 +58,35 @@ const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
className="mb-0"
/>
<div className="pt-6 text-sm">
<div className="mb-6 rounded-md bg-gray-100 p-2">
<h1 className="mb-2 ml-1 font-medium text-gray-900">{t("available_variables")}</h1>
<div className="bg-subtle mb-6 rounded-md p-2">
<h1 className="text-emphasis mb-2 ml-1 font-medium">{t("available_variables")}</h1>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Event type title}`}</p>
<p className="text-gray-900">{t("event_name_info")}</p>
<p className="text-subtle ml-1 mr-5 w-28">{`{Event type title}`}</p>
<p className="text-emphasis">{t("event_name_info")}</p>
</div>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Organiser}`}</p>
<p className="text-gray-900">{t("your_full_name")}</p>
<p className="text-subtle ml-1 mr-5 w-28">{`{Organiser}`}</p>
<p className="text-emphasis">{t("your_full_name")}</p>
</div>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Scheduler}`}</p>
<p className="text-gray-900">{t("scheduler_full_name")}</p>
<p className="text-subtle ml-1 mr-5 w-28">{`{Scheduler}`}</p>
<p className="text-emphasis">{t("scheduler_full_name")}</p>
</div>
<div className="mb-1 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Location}`}</p>
<p className="text-gray-900">{t("location_info")}</p>
<p className="text-subtle ml-1 mr-5 w-28">{`{Location}`}</p>
<p className="text-emphasis">{t("location_info")}</p>
</div>
</div>
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
<div
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center"
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center dark:invert"
style={{
backgroundImage: "url(/calendar-preview.svg)",
}}>
<div className="m-auto flex items-center justify-center self-stretch">
<div className="mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black bg-gray-100 text-[12px] leading-3">
<p className="overflow-hidden text-ellipsis p-1.5 font-medium text-gray-900">{previewText}</p>
<p className="ml-1.5 text-[10px] font-normal text-gray-600">8 - 10 AM</p>
<div className="bg-subtle mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black text-[12px] leading-3">
<p className="text-emphasis overflow-hidden text-ellipsis p-1.5 font-medium">{previewText}</p>
<p className="text-default ml-1.5 text-[10px] font-normal">8 - 10 AM</p>
</div>
</div>
</div>
@@ -8,14 +8,17 @@ import { v5 as uuidv5 } from "uuid";
import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } from "@calcom/core/event";
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
import { Edit, Copy } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -56,6 +59,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const noShowFeeEnabled = eventType.metadata.apps?.stripe?.paymentOption === "HOLD";
useEffect(() => {
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
@@ -77,11 +81,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
);
};
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const eventNamePlaceholder = getEventName({
...eventNameObject,
eventName: formMethods.watch("eventName"),
});
const successRedirectUrlLocked = shouldLockDisableProps("successRedirectUrl");
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
const closeEventNameTip = () => setShowEventNameTip(false);
const setEventName = (value: string) => formMethods.setValue("eventName", value);
@@ -99,7 +111,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Link
href="/apps/categories/calendar"
target="_blank"
className="text-sm text-gray-600 hover:text-gray-900">
className="hover:text-emphasis text-default text-sm">
{t("add_another_calendar")}
</Link>
</div>
@@ -118,44 +130,51 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
)}
/>
</div>
<p className="text-sm text-gray-600">{t("select_which_cal")}</p>
<p className="text-default text-sm">{t("select_which_cal")}</p>
</div>
)}
<div className="w-full">
<TextField
label={t("event_name_in_calendar")}
type="text"
{...shouldLockDisableProps("eventName")}
placeholder={eventNamePlaceholder}
defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")}
addOnSuffix={
<Button
type="button"
StartIcon={FiEdit}
variant="icon"
color="minimal"
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
onClick={() => setShowEventNameTip((old) => !old)}
size="sm"
aria-label="edit custom name"
/>
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
onClick={() => setShowEventNameTip((old) => !old)}>
<Edit className="h-4 w-4" />
</Button>
}
/>
</div>
<hr />
<hr className="border-subtle" />
<FormBuilder
title={t("booking_questions_title")}
description={t("booking_questions_description")}
addFieldLabel={t("add_a_booking_question")}
formProp="bookingFields"
{...shouldLockDisableProps("bookingFields")}
dataStore={{
options: {
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
},
}}
/>
<hr />
<hr className="border-subtle" />
<RequiresConfirmationController
eventType={eventType}
seatsEnabled={seatsEnabled}
metadata={eventType.metadata}
requiresConfirmation={requiresConfirmation}
onRequiresConfirmation={setRequiresConfirmation}
/>
<hr />
<hr className="border-subtle" />
<Controller
name="hideCalendarNotes"
control={formMethods.control}
@@ -163,13 +182,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
render={({ field: { value, onChange } }) => (
<SettingsToggle
title={t("disable_notes")}
{...shouldLockDisableProps("hideCalendarNotes")}
description={t("disable_notes_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
)}
/>
<hr />
<hr className="border-subtle" />
<Controller
name="successRedirectUrl"
control={formMethods.control}
@@ -177,48 +197,43 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<>
<SettingsToggle
title={t("redirect_success_booking")}
{...successRedirectUrlLocked}
description={t("redirect_url_description")}
checked={redirectUrlVisible}
onCheckedChange={(e) => {
setRedirectUrlVisible(e);
onChange(e ? value : "");
}}>
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
<div className="lg:-ml-2">
{/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
<div className="lg:-ml-2 lg:-mb-2">
<TextField
className="w-full"
label={t("redirect_success_booking")}
labelSrOnly
disabled={successRedirectUrlLocked.disabled}
placeholder={t("external_redirect_url")}
required={redirectUrlVisible}
type="text"
defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")}
/>
<div className="mt-2 flex">
<Checkbox
description={t("disable_success_page")}
// Disable if it's not Self Hosted or if the redirect url is not set
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")}
{...formMethods.register("metadata.disableSuccessPage")}
/>
{/*TODO: Extract it out into a component when used more than once*/}
{!IS_SELF_HOSTED && (
<Link href="https://cal.com/pricing" target="_blank">
<Badge variant="orange" className="ml-2">
Platform Only
</Badge>
</Link>
)}
<div
className={classNames(
"p-1 text-sm text-orange-600",
formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
)}>
{t("redirect_url_warning")}
</div>
</div>
</SettingsToggle>
</>
)}
/>
<hr />
<hr className="border-subtle" />
<SettingsToggle
data-testid="hashedLinkCheck"
title={t("private_link")}
{...shouldLockDisableProps("hashedLinkCheck")}
description={t("private_link_description", { appName: APP_NAME })}
checked={hashedLinkVisible}
onCheckedChange={(e) => {
@@ -240,6 +255,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Button
color="minimal"
size="sm"
type="button"
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
aria-label="copy link"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
@@ -247,69 +266,76 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
} else {
showToast(t("enabled_after_update_description"), "warning");
}
}}
className="hover:stroke-3 hover:bg-transparent hover:text-black"
type="button">
<FiCopy />
}}>
<Copy className="h-4 w-4" />
</Button>
</Tooltip>
}
/>
</div>
</SettingsToggle>
<hr />
<hr className="border-subtle" />
<Controller
name="seatsPerTimeSlotEnabled"
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<SettingsToggle
title={t("offer_seats")}
description={t("offer_seats_description")}
checked={value}
onCheckedChange={(e) => {
// Enabling seats will disable guests and requiring confirmation until fully supported
if (e) {
toggleGuests(false);
formMethods.setValue("requiresConfirmation", false);
setRequiresConfirmation(false);
formMethods.setValue("seatsPerTimeSlot", 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
toggleGuests(true);
}
onChange(e);
}}>
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div className="lg:-ml-2">
<TextField
required
name="seatsPerTimeSlot"
labelSrOnly
label={t("number_of_seats")}
type="number"
defaultValue={value || 2}
min={1}
addOnSuffix={<>{t("seats")}</>}
onChange={(e) => {
onChange(Math.abs(Number(e.target.value)));
}}
/>
<div className="mt-2">
<Checkbox
description={t("show_attendees")}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
defaultChecked={!!eventType.seatsShowAttendees}
<>
<SettingsToggle
data-testid="offer-seats-toggle"
title={t("offer_seats")}
{...seatsLocked}
description={t("offer_seats_description")}
checked={value}
disabled={noShowFeeEnabled}
onCheckedChange={(e) => {
// Enabling seats will disable guests and requiring confirmation until fully supported
if (e) {
toggleGuests(false);
formMethods.setValue("requiresConfirmation", false);
setRequiresConfirmation(false);
formMethods.setValue("seatsPerTimeSlot", 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
toggleGuests(true);
}
onChange(e);
}}>
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div className="lg:-ml-2">
<TextField
required
name="seatsPerTimeSlot"
labelSrOnly
label={t("number_of_seats")}
type="number"
disabled={seatsLocked.disabled}
defaultValue={value || 2}
min={1}
className="w-24"
addOnSuffix={<>{t("seats")}</>}
onChange={(e) => {
onChange(Math.abs(Number(e.target.value)));
}}
/>
<div className="mt-2">
<Checkbox
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
defaultChecked={!!eventType.seatsShowAttendees}
/>
</div>
</div>
</div>
)}
/>
</SettingsToggle>
)}
/>
</SettingsToggle>
{noShowFeeEnabled && <Alert severity="warning" title={t("seats_and_no_show_fee_error")} />}
</>
)}
/>
+44 -21
View File
@@ -5,10 +5,11 @@ import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppConte
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
import type { EventTypeAppsList } from "@calcom/app-store/utils";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, EmptyScreen } from "@calcom/ui";
import { FiGrid } from "@calcom/ui/components/icon";
import { Button, EmptyScreen, Alert } from "@calcom/ui";
import { Grid, Lock } from "@calcom/ui/components/icon";
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
EventTypeAppCardComponentProps["eventType"];
@@ -55,19 +56,39 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
};
};
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
return (
<>
<div>
<div className="before:border-0">
{!installedApps?.length && isManagedEventType && (
<Alert
severity="neutral"
className="mb-2"
title={t("locked_for_members")}
message={t("locked_apps_description")}
/>
)}
{!isLoading && !installedApps?.length ? (
<EmptyScreen
Icon={FiGrid}
Icon={Grid}
headline={t("empty_installed_apps_headline")}
description={t("empty_installed_apps_description")}
buttonRaw={
<Button target="_blank" color="secondary" href="/apps">
{t("empty_installed_apps_button")}{" "}
</Button>
isChildrenManagedEventType && !isManagedEventType ? (
<Button StartIcon={Lock} color="secondary" disabled>
{t("locked_by_admin")}
</Button>
) : (
<Button target="_blank" color="secondary" href="/apps">
{t("empty_installed_apps_button")}{" "}
</Button>
)
}
/>
) : null}
@@ -82,22 +103,24 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
))}
</div>
</div>
<div>
{!isLoading && notInstalledApps?.length ? (
<h2 className="mt-0 mb-2 text-lg font-semibold text-gray-900">Available Apps</h2>
) : null}
<div className="before:border-0">
{notInstalledApps?.map((app) => (
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug}
app={app}
eventType={eventType}
/>
))}
{!shouldLockDisableProps("apps").disabled && (
<div>
{!isLoading && notInstalledApps?.length ? (
<h2 className="text-emphasis mt-0 mb-2 text-lg font-semibold">{t("available_apps")}</h2>
) : null}
<div className="before:border-0">
{notInstalledApps?.map((app) => (
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug}
app={app}
eventType={eventType}
/>
))}
</div>
</div>
</div>
)}
</>
);
};
@@ -1,27 +1,29 @@
import type { FormValues } from "pages/event-types/[type]";
import { SchedulingType } from "@prisma/client";
import type { FormValues, EventTypeSetup } from "pages/event-types/[type]";
import { Controller, useFormContext } from "react-hook-form";
import type { OptionProps, SingleValueProps } from "react-select";
import { components } from "react-select";
import dayjs from "@calcom/dayjs";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { NewScheduleButton } from "@calcom/features/schedules";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
import { ExternalLink, Globe, Clock } from "@calcom/ui/components/icon";
type AvailabilityOption = {
label: string;
value: number;
isDefault: boolean;
isManaged?: boolean;
};
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
const { label, isDefault } = props.data;
const { label, isDefault, isManaged = false } = props.data;
const { t } = useLocale();
return (
<components.Option {...props}>
@@ -31,12 +33,17 @@ const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
{t("default")}
</Badge>
)}
{isManaged && (
<Badge variant="gray" className="ml-2">
{t("managed")}
</Badge>
)}
</components.Option>
);
};
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
const { label, isDefault } = props.data;
const { label, isDefault, isManaged = false } = props.data;
const { t } = useLocale();
return (
<components.SingleValue {...props}>
@@ -46,49 +53,39 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
{t("default")}
</Badge>
)}
{isManaged && (
<Badge variant="gray" className="ml-2">
{t("managed")}
</Badge>
)}
</components.SingleValue>
);
};
const AvailabilitySelect = ({
className = "",
options,
value,
...props
}: {
className?: string;
name: string;
value: number;
value: AvailabilityOption | undefined;
options: AvailabilityOption[];
isDisabled?: boolean;
onBlur: () => void;
onChange: (value: AvailabilityOption | null) => void;
}) => {
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
const { t } = useLocale();
if (isLoading) {
return <SelectSkeletonLoader />;
}
const schedules = data?.schedules || [];
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
}));
const value = options.find((option) =>
props.value
? option.value === props.value
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
);
return (
<Select
placeholder={t("select")}
options={options}
isDisabled={props.isDisabled}
isSearchable={false}
onChange={props.onChange}
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
value={value}
defaultValue={value}
components={{ Option, SingleValue }}
isMulti={false}
/>
@@ -100,7 +97,13 @@ const format = (date: Date, hour12: boolean) =>
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
);
const EventTypeScheduleDetails = () => {
const EventTypeScheduleDetails = ({
isManagedEventType,
selectedScheduleValue,
}: {
isManagedEventType: boolean;
selectedScheduleValue: AvailabilityOption | undefined;
}) => {
const { data: loggedInUser } = useMeQuery();
const timeFormat = loggedInUser?.timeFormat;
const { t, i18n } = useLocale();
@@ -108,15 +111,18 @@ const EventTypeScheduleDetails = () => {
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
{
scheduleId: scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
isManagedEventType,
},
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
);
const filterDays = (dayNum: number) =>
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
return (
<div className="space-y-4 rounded border px-6 pb-4">
<div className="border-default space-y-4 rounded border px-6 pb-4">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
@@ -124,8 +130,8 @@ const EventTypeScheduleDetails = () => {
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span
className={classNames(
"w-20 font-medium sm:w-32",
!isAvailable && "text-gray-500 opacity-50"
"w-20 font-medium sm:w-32 ",
!isAvailable ? "text-subtle line-through" : "text-default"
)}>
{day}
</span>
@@ -134,34 +140,34 @@ const EventTypeScheduleDetails = () => {
) : isAvailable ? (
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="flex items-center leading-4">
<div key={i} className="text-default flex items-center leading-4">
<span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ltr:ml-4 rtl:mr-4">-</span>
<span className="ms-4">-</span>
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
</div>
) : (
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
<span className="text-subtle ml-6 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr />
<hr className="border-subtle" />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
{!!schedule?.id && (
{!!schedule?.id && !schedule.isManaged && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
color="minimal"
EndIcon={FiExternalLink}
EndIcon={ExternalLink}
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
@@ -172,34 +178,112 @@ const EventTypeScheduleDetails = () => {
);
};
const EventTypeSchedule = () => {
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
const { shouldLockIndicator, shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } =
useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const { watch } = useFormContext<FormValues>();
const watchSchedule = watch("schedule");
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
if (!data?.schedules.length && !isLoading)
return (
<EmptyScreen
Icon={Clock}
headline={t("new_schedule_heading")}
description={t("new_schedule_description")}
buttonRaw={<NewScheduleButton fromEventType />}
border={false}
/>
);
const schedules = data?.schedules || [];
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
isManaged: false,
}));
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
if (isManagedEventType) {
options.push({
value: 0,
label: t("members_default_schedule"),
isDefault: false,
isManaged: false,
});
}
// We are showing a managed event for a member and team owner selected their own schedule, so adding
// the managed schedule option
if (
isChildrenManagedEventType &&
watchSchedule &&
!schedules.find((schedule) => schedule.id === watchSchedule)
) {
options.push({
value: watchSchedule,
label: eventType.scheduleName ?? t("default_schedule_name"),
isDefault: false,
isManaged: false,
});
}
const value = options.find((option) =>
watchSchedule
? option.value === watchSchedule
: isManagedEventType
? option.value === 0
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
);
return (
<div className="space-y-4">
<div>
<label htmlFor="availability" className="mb-2 block text-sm font-medium leading-none text-gray-700">
<label htmlFor="availability" className="text-default mb-2 block text-sm font-medium leading-none">
{t("availability")}
{shouldLockIndicator("availability")}
</label>
<Controller
name="schedule"
render={({ field }) => (
<AvailabilitySelect
value={field.value}
onBlur={field.onBlur}
name={field.name}
onChange={(selected) => {
field.onChange(selected?.value || null);
}}
/>
<>
<AvailabilitySelect
value={value}
options={options}
onBlur={field.onBlur}
isDisabled={shouldLockDisableProps("schedule").disabled}
name={field.name}
onChange={(selected) => {
field.onChange(selected?.value || null);
}}
/>
</>
)}
/>
</div>
<EventTypeScheduleDetails />
{value?.value !== 0 ? (
<EventTypeScheduleDetails
selectedScheduleValue={value}
isManagedEventType={isManagedEventType || isChildrenManagedEventType}
/>
) : (
isManagedEventType && (
<p className="!mt-2 ml-1 text-sm text-gray-600">{t("members_default_schedule_description")}</p>
)
)}
</div>
);
};
const UseCommonScheduleSettingsToggle = () => {
const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
const { resetField, setValue } = useFormContext<FormValues>();
return (
@@ -218,13 +302,23 @@ const UseCommonScheduleSettingsToggle = () => {
}}
title={t("choose_common_schedule_team_event")}
description={t("choose_common_schedule_team_event_description")}>
<EventTypeSchedule />
<EventTypeSchedule eventType={eventType} />
</SettingsToggle>
)}
/>
);
};
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />;
export const EventAvailabilityTab = ({
eventType,
isTeamEvent,
}: {
eventType: EventTypeSetup;
isTeamEvent: boolean;
}) => {
return isTeamEvent && eventType.schedulingType !== SchedulingType.MANAGED ? (
<UseCommonScheduleSettingsToggle eventType={eventType} />
) : (
<EventTypeSchedule eventType={eventType} />
);
};
+106 -48
View File
@@ -7,6 +7,7 @@ import type { UseFormRegisterReturn } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { SingleValue } from "react-select";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { classNames } from "@calcom/lib";
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
@@ -14,17 +15,8 @@ import findDurationType from "@calcom/lib/findDurationType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { PeriodType } from "@calcom/prisma/client";
import type { IntervalLimit } from "@calcom/types/Calendar";
import {
Button,
DateRangePicker,
Input,
InputField,
Label,
Select,
SettingsToggle,
TextField,
} from "@calcom/ui";
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
import { Plus, Trash } from "@calcom/ui/components/icon";
const MinimumBookingNoticeInput = React.forwardRef<
HTMLInputElement,
@@ -78,6 +70,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
<div className="w-1/2 md:w-full">
<InputField
required
disabled={passThroughProps.disabled}
defaultValue={minimumBookingNoticeDisplayValues.value}
onChange={(e) =>
setMinimumBookingNoticeDisplayValues({
@@ -88,12 +81,14 @@ const MinimumBookingNoticeInput = React.forwardRef<
label={t("minimum_booking_notice")}
type="number"
placeholder="0"
min={0}
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
/>
<input type="hidden" ref={ref} {...passThroughProps} />
</div>
<Select
isSearchable={false}
isDisabled={passThroughProps.disabled}
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
defaultValue={durationTypeOptions.find(
(option) => option.value === minimumBookingNoticeDisplayValues.type
@@ -145,12 +140,30 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
defaultValue: periodType?.type,
});
const { shouldLockIndicator, shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
const periodTypeLocked = shouldLockDisableProps("periodType");
const optionsPeriod = [
{ value: 1, label: t("calendar_days") },
{ value: 0, label: t("business_days") },
];
return (
<div className="space-y-8">
<div className="space-y-4 lg:space-y-8">
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="w-full">
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
<Label htmlFor="beforeBufferTime">
{t("before_event")}
{shouldLockIndicator("bookingLimits")}
</Label>
<Controller
name="beforeBufferTime"
control={formMethods.control}
@@ -169,6 +182,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return (
<Select
isSearchable={false}
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
onChange={(val) => {
if (val) onChange(val.value);
}}
@@ -182,7 +196,10 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
</div>
<div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
<Label htmlFor="afterBufferTime">
{t("after_event")}
{shouldLockIndicator("bookingLimits")}
</Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
@@ -201,6 +218,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return (
<Select
isSearchable={false}
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
onChange={(val) => {
if (val) onChange(val.value);
}}
@@ -216,11 +234,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</div>
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="w-full">
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
<Label htmlFor="minimumBookingNotice">
{t("minimum_booking_notice")}
{shouldLockIndicator("minimumBookingNotice")}
</Label>
<MinimumBookingNoticeInput
disabled={shouldLockDisableProps("minimumBookingNotice").disabled}
{...formMethods.register("minimumBookingNotice")}
/>
</div>
<div className="w-full">
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
<Label htmlFor="slotInterval">
{t("slot_interval")}
{shouldLockIndicator("slotInterval")}
</Label>
<Controller
name="slotInterval"
control={formMethods.control}
@@ -238,6 +265,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return (
<Select
isSearchable={false}
isDisabled={shouldLockDisableProps("slotInterval").disabled}
onChange={(val) => {
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
}}
@@ -253,13 +281,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</div>
</div>
</div>
<hr />
<hr className="border-subtle" />
<Controller
name="bookingLimits"
control={formMethods.control}
render={({ field: { value } }) => (
<SettingsToggle
title={t("limit_booking_frequency")}
{...bookingLimitsLocked}
description={t("limit_booking_frequency_description")}
checked={Object.keys(value ?? {}).length > 0}
onCheckedChange={(active) => {
@@ -271,11 +300,16 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
formMethods.setValue("bookingLimits", {});
}
}}>
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
<IntervalLimitsManager
disabled={bookingLimitsLocked.disabled}
propertyName="bookingLimits"
defaultLimit={1}
step={1}
/>
</SettingsToggle>
)}
/>
<hr />
<hr className="border-subtle" />
<Controller
name="durationLimits"
control={formMethods.control}
@@ -283,6 +317,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<SettingsToggle
title={t("limit_total_booking_duration")}
description={t("limit_total_booking_duration_description")}
{...durationLimitsLocked}
checked={Object.keys(value ?? {}).length > 0}
onCheckedChange={(active) => {
if (active) {
@@ -296,13 +331,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<IntervalLimitsManager
propertyName="durationLimits"
defaultLimit={60}
disabled={durationLimitsLocked.disabled}
step={15}
textFieldSuffix={t("minutes")}
/>
</SettingsToggle>
)}
/>
<hr />
<hr className="border-subtle" />
<Controller
name="periodType"
control={formMethods.control}
@@ -310,49 +346,64 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<SettingsToggle
title={t("limit_future_bookings")}
description={t("limit_future_bookings_description")}
{...periodTypeLocked}
checked={value && value !== "UNLIMITED"}
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
<RadioGroup.Root
defaultValue={watchPeriodType}
value={watchPeriodType}
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
{PERIOD_TYPES.map((period) => {
{PERIOD_TYPES.filter((opt) =>
periodTypeLocked.disabled ? watchPeriodType === opt.type : true
).map((period) => {
if (period.type === "UNLIMITED") return null;
return (
<div
className={classNames(
"mb-2 flex flex-wrap items-center text-sm",
"text-default mb-2 flex flex-wrap items-center text-sm",
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
)}
key={period.type}>
<RadioGroup.Item
id={period.type}
value={period.type}
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
{!periodTypeLocked.disabled && (
<RadioGroup.Item
id={period.type}
value={period.type}
className="min-w-4 bg-default border-default flex h-4 w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
</RadioGroup.Item>
)}
{period.prefix ? <span>{period.prefix}&nbsp;</span> : null}
{period.type === "ROLLING" && (
<div className="flex h-9">
<Input
<div className="flex items-center">
<TextField
labelSrOnly
type="number"
className="block w-16 rounded-md border-gray-300 py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
placeholder="30"
disabled={periodTypeLocked.disabled}
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
/>
<select
id=""
className="block h-9 w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
{...formMethods.register("periodCountCalendarDays")}
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
<option value="1">{t("calendar_days")}</option>
<option value="0">{t("business_days")}</option>
</select>
<Select
options={optionsPeriod}
isSearchable={false}
isDisabled={periodTypeLocked.disabled}
onChange={(opt) => {
formMethods.setValue(
"periodCountCalendarDays",
opt?.value.toString() as "0" | "1"
);
}}
defaultValue={
optionsPeriod.find(
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
) ?? optionsPeriod[0]
}
/>
</div>
)}
{period.type === "RANGE" && (
<div className="inline-flex space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse">
<div className="ms-2 me-2 inline-flex space-x-2 rtl:space-x-reverse">
<Controller
name="periodDates"
control={formMethods.control}
@@ -361,6 +412,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
endDate={formMethods.getValues("periodDates").endDate}
disabled={periodTypeLocked.disabled}
onDatesChange={({ startDate, endDate }) => {
formMethods.setValue("periodDates", {
startDate,
@@ -372,9 +424,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
</div>
)}
{period.suffix ? (
<span className="ltr:ml-2 ltr:mr-2 rtl:ml-2">&nbsp;{period.suffix}</span>
) : null}
{period.suffix ? <span className="ms-2 me-2">&nbsp;{period.suffix}</span> : null}
</div>
);
})}
@@ -401,6 +451,7 @@ type IntervalLimitItemProps = {
step: number;
value: number;
textFieldSuffix?: string;
disabled?: boolean;
selectOptions: { value: keyof IntervalLimit; label: string }[];
hasDeleteButton?: boolean;
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
@@ -415,6 +466,7 @@ const IntervalLimitItem = ({
textFieldSuffix,
selectOptions,
hasDeleteButton,
disabled,
onDelete,
onLimitChange,
onIntervalSelect,
@@ -424,8 +476,10 @@ const IntervalLimitItem = ({
<TextField
required
type="number"
containerClassName={`${textFieldSuffix ? "w-36" : "w-16"} -mb-1`}
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
className="mb-0 !h-auto"
placeholder={`${value}`}
disabled={disabled}
min={step}
step={step}
defaultValue={value}
@@ -435,11 +489,12 @@ const IntervalLimitItem = ({
<Select
options={selectOptions}
isSearchable={false}
isDisabled={disabled}
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
onChange={onIntervalSelect}
/>
{hasDeleteButton && (
<Button variant="icon" StartIcon={FiTrash} color="destructive" onClick={() => onDelete(limitKey)} />
{hasDeleteButton && !disabled && (
<Button variant="icon" StartIcon={Trash} color="destructive" onClick={() => onDelete(limitKey)} />
)}
</div>
);
@@ -450,6 +505,7 @@ type IntervalLimitsManagerProps<K extends "durationLimits" | "bookingLimits"> =
defaultLimit: number;
step: number;
textFieldSuffix?: string;
disabled?: boolean;
};
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
@@ -457,6 +513,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
defaultLimit,
step,
textFieldSuffix,
disabled,
}: IntervalLimitsManagerProps<K>) => {
const { watch, setValue, control } = useFormContext<FormValues>();
const watchIntervalLimits = watch(propertyName);
@@ -506,6 +563,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
limitKey={limitKey}
step={step}
value={value}
disabled={disabled}
textFieldSuffix={textFieldSuffix}
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
@@ -536,8 +594,8 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
/>
);
})}
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && (
<Button color="minimal" StartIcon={FiPlus} onClick={addLimit}>
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && !disabled && (
<Button color="minimal" StartIcon={Plus} onClick={addLimit}>
{t("add_limit")}
</Button>
)}
@@ -11,7 +11,7 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
return (
<div className="">
<RecurringEventController paymentEnabled={requirePayment} recurringEvent={eventType.recurringEvent} />
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
</div>
);
};
+138 -44
View File
@@ -4,20 +4,31 @@ import { isValidPhoneNumber } from "libphonenumber-js";
import { Trans } from "next-i18next";
import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { useEffect, useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form";
import type { MultiValue } from "react-select";
import { z } from "zod";
import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import { slugify } from "@calcom/lib/slugify";
import turndown from "@calcom/lib/turndownService";
import { Button, Editor, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
import {
Button,
Label,
Select,
SettingsToggle,
Skeleton,
TextField,
Editor,
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { Edit2, Check, X, Plus } from "@calcom/ui/components/icon";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
@@ -35,14 +46,57 @@ const getLocationFromType = (
}
};
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => {
for (const locationType of options) {
for (const location of locationType.options) {
if (location.value === type && location.disabled === false) {
return location;
}
}
}
const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
const locationAvailable =
props.eventType.locations &&
props.eventType.locations.length > 0 &&
props.locationOptions.some((op) =>
op.options.find((opt) => opt.value === props.eventType.locations[0].type)
);
const locationDetails = props.eventType.locations &&
props.eventType.locations.length > 0 &&
!locationAvailable && {
slug: props.eventType.locations[0].type
.replace("integrations:", "")
.replace(":", "-")
.replace("_video", ""),
name: props.eventType.locations[0].type
.replace("integrations:", "")
.replace(":", " ")
.replace("_video", "")
.split(" ")
.map((word) => word[0].toUpperCase() + word.slice(1))
.join(" "),
};
return { locationAvailable, locationDetails };
};
interface DescriptionEditorProps {
description?: string | null;
editable?: boolean;
}
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")}
editable={props.editable}
/>
) : (
<SkeletonContainer>
<SkeletonText className="block h-24 w-full" />
</SkeletonContainer>
);
};
export const EventSetupTab = (
@@ -53,12 +107,16 @@ export const EventSetupTab = (
) => {
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const { eventType, locationOptions, team, destinationCalendar } = props;
const { eventType, team, destinationCalendar } = props;
const [showLocationModal, setShowLocationModal] = useState(false);
const [editingLocationType, setEditingLocationType] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
const locationOptions = props.locationOptions.filter((option) => {
return !team ? option.label !== "Conferencing" : true;
});
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
value: mins,
label: t("multiple_duration_mins", { count: mins }),
@@ -143,6 +201,13 @@ export const EventSetupTab = (
resolver: zodResolver(locationFormSchema),
});
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const Locations = () => {
const { t } = useLocale();
@@ -157,6 +222,12 @@ export const EventSetupTab = (
return true;
});
const defaultValue = isManagedEventType
? locationOptions.find((op) => op.label === t("default"))?.options[0]
: undefined;
const { locationDetails, locationAvailable } = getLocationInfo(props);
return (
<div className="w-full">
{validLocations.length === 0 && (
@@ -164,6 +235,8 @@ export const EventSetupTab = (
<LocationSelect
placeholder={t("select")}
options={locationOptions}
isDisabled={shouldLockDisableProps("locations").disabled}
defaultValue={defaultValue}
isSearchable={false}
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
menuPlacement="auto"
@@ -199,7 +272,7 @@ export const EventSetupTab = (
return (
<li
key={`${location.type}${index}`}
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
className="border-default text-default mb-2 rounded-md border py-1.5 px-2 hover:cursor-pointer">
<div className="flex items-center justify-between">
<div className="flex items-center">
<img
@@ -207,7 +280,7 @@ export const EventSetupTab = (
className="h-4 w-4"
alt={`${eventLocationType.label} logo`}
/>
<span className="line-clamp-1 text-sm ltr:ml-1 rtl:mr-1">{eventLabel}</span>
<span className="line-clamp-1 ms-1 text-sm">{eventLabel}</span>
</div>
<div className="flex">
<button
@@ -221,11 +294,11 @@ export const EventSetupTab = (
openLocationModal(location.type);
}}
aria-label={t("edit")}
className="mr-1 p-1 text-gray-500 hover:text-gray-900">
<FiEdit2 className="h-4 w-4" />
className="hover:text-emphasis text-subtle mr-1 p-1">
<Edit2 className="h-4 w-4" />
</button>
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
<FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
<X className="border-l-1 hover:text-emphasis text-subtle h-6 w-6 pl-1 " />
</button>
</div>
</div>
@@ -236,8 +309,8 @@ export const EventSetupTab = (
(location) =>
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
) && (
<div className="flex text-sm text-gray-600">
<FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
<div className="text-default flex text-sm">
<Check className="mt-0.5 mr-1.5 h-2 w-2.5" />
<Trans i18nKey="event_type_requres_google_cal">
<p>
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
@@ -251,11 +324,19 @@ export const EventSetupTab = (
</Trans>
</div>
)}
{validLocations.length > 0 && (
{isChildrenManagedEventType && !locationAvailable && locationDetails && (
<p className="pl-1 text-sm leading-none text-red-600">
{t("app_not_connected", { appName: locationDetails.name })}{" "}
<a className="underline" href={`${CAL_URL}/apps/${locationDetails.slug}`}>
{t("connect_now")}
</a>
</p>
)}
{validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && (
<li>
<Button
data-testid="add-location"
StartIcon={FiPlus}
StartIcon={Plus}
color="minimal"
onClick={() => setShowLocationModal(true)}>
{t("add_location")}
@@ -268,27 +349,33 @@ export const EventSetupTab = (
);
};
const lengthLockedProps = shouldLockDisableProps("length");
const descriptionLockedProps = shouldLockDisableProps("description");
return (
<div>
<div className="space-y-8">
<TextField
required
label={t("title")}
{...shouldLockDisableProps("title")}
defaultValue={eventType.title}
{...formMethods.register("title")}
/>
<div>
<Label>{t("description")}</Label>
<Editor
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
<Label>
{t("description")}
{shouldLockIndicator("description")}
</Label>
<DescriptionEditor
description={eventType?.description}
editable={!descriptionLockedProps.disabled}
/>
</div>
<TextField
required
label={t("URL")}
{...shouldLockDisableProps("slug")}
defaultValue={eventType.slug}
addOnLeading={
<>
@@ -341,12 +428,14 @@ export const EventSetupTab = (
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("default_duration")}
{shouldLockIndicator("length")}
</Skeleton>
<Select
value={defaultDuration}
isSearchable={false}
name="length"
className="text-sm"
isDisabled={lengthLockedProps.disabled}
noOptionsMessage={() => t("default_duration_no_options")}
options={selectedMultipleDuration}
onChange={(option) => {
@@ -362,32 +451,36 @@ export const EventSetupTab = (
<TextField
required
type="number"
{...lengthLockedProps}
label={t("duration")}
defaultValue={eventType.length ?? 15}
{...formMethods.register("length")}
addOnSuffix={<>{t("minutes")}</>}
/>
)}
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
<SettingsToggle
title={t("allow_booker_to_select_duration")}
checked={multipleDuration !== undefined}
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
} else {
setMultipleDuration([]);
formMethods.setValue("metadata.multipleDuration", []);
formMethods.setValue("length", 0);
}
}}
/>
</div>
{!lengthLockedProps.disabled && (
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
<SettingsToggle
title={t("allow_booker_to_select_duration")}
checked={multipleDuration !== undefined}
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
} else {
setMultipleDuration([]);
formMethods.setValue("metadata.multipleDuration", []);
formMethods.setValue("length", 0);
}
}}
/>
</div>
)}
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("location")}
{shouldLockIndicator("locations")}
</Skeleton>
<Controller
@@ -401,6 +494,7 @@ export const EventSetupTab = (
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
<EditLocationDialog
isTeamEvent={!!team}
isOpenDialog={showLocationModal}
setShowLocationModal={setShowLocationModal}
saveLocation={saveLocation}
+89 -6
View File
@@ -1,4 +1,4 @@
import type { SchedulingType } from "@prisma/client";
import { SchedulingType } from "@prisma/client";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef } from "react";
import type { ComponentProps } from "react";
@@ -7,24 +7,46 @@ import type { Options } from "react-select";
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label, Select } from "@calcom/ui";
interface IMemberToValue {
interface IUserToValue {
id: number | null;
name: string | null;
username: string | null;
email: string;
}
const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
const mapUserToValue = ({ id, name, username, email }: IUserToValue) => ({
value: `${id || ""}`,
label: `${name || ""}`,
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
email,
});
export const mapMemberToChildrenOption = (
member: EventTypeSetupProps["teamMembers"][number],
slug: string
) => {
return {
slug,
hidden: false,
created: false,
owner: {
id: member.id,
name: member.name ?? "",
email: member.email,
username: member.username ?? "",
membership: member.membership,
eventTypeSlugs: member.eventTypes ?? [],
},
value: `${member.id ?? ""}`,
label: member.name ?? "",
};
};
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
if (a.label < b.label) {
return -1;
@@ -35,6 +57,40 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
return 0;
};
const ChildrenEventTypesList = ({
options = [],
value,
onChange,
...rest
}: {
value: ReturnType<typeof mapMemberToChildrenOption>[];
onChange?: (options: ReturnType<typeof mapMemberToChildrenOption>[]) => void;
options?: Options<ReturnType<typeof mapMemberToChildrenOption>>;
} & Omit<Partial<ComponentProps<typeof ChildrenEventTypeSelect>>, "onChange" | "value">) => {
const { t } = useLocale();
return (
<div className="flex flex-col space-y-5">
<div>
<Label>{t("assign_to")}</Label>
<ChildrenEventTypeSelect
onChange={(options) => {
onChange &&
onChange(
options.map((option) => ({
...option,
}))
);
}}
value={value}
options={options.filter((opt) => !value.find((val) => val.owner.id.toString() === opt.value))}
controlShouldRenderValue={false}
{...rest}
/>
</div>
</div>
);
};
const CheckedHostField = ({
labelText,
placeholder,
@@ -52,7 +108,7 @@ const CheckedHostField = ({
options?: Options<CheckedSelectOption>;
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
return (
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
<div className="bg-muted flex flex-col space-y-5 p-4">
<div>
<Label>{labelText}</Label>
<CheckedTeamSelect
@@ -123,6 +179,21 @@ const RoundRobinHosts = ({
);
};
const ChildrenEventTypes = ({
childrenEventTypeOptions,
}: {
childrenEventTypeOptions: ReturnType<typeof mapMemberToChildrenOption>[];
}) => {
return (
<Controller<FormValues>
name="children"
render={({ field: { onChange, value } }) => (
<ChildrenEventTypesList value={value} options={childrenEventTypeOptions} onChange={onChange} />
)}
/>
);
};
const Hosts = ({
teamMembers,
}: {
@@ -189,6 +260,7 @@ const Hosts = ({
/>*/}
</>
),
MANAGED: <></>,
};
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
}}
@@ -196,7 +268,11 @@ const Hosts = ({
);
};
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
export const EventTeamTab = ({
team,
teamMembers,
eventType,
}: Pick<EventTypeSetupProps, "teamMembers" | "team" | "eventType">) => {
const { t } = useLocale();
const schedulingTypeOptions: {
@@ -216,9 +292,13 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
},
];
const teamMembersOptions = teamMembers.map(mapUserToValue);
const childrenEventTypeOptions = teamMembers.map((member) => {
return mapMemberToChildrenOption(member, eventType.slug);
});
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
return (
<div>
{team && (
{team && !isManagedEventType && (
<div className="space-y-5">
<div className="flex flex-col">
<Label>{t("scheduling_type")}</Label>
@@ -239,6 +319,9 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
<Hosts teamMembers={teamMembersOptions} />
</div>
)}
{team && isManagedEventType && (
<ChildrenEventTypes childrenEventTypeOptions={childrenEventTypeOptions} />
)}
</div>
);
};
@@ -1,16 +1,17 @@
import type { Webhook } from "@prisma/client";
import { Webhook as TbWebhook } from "lucide-react";
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import { useState } from "react";
import { TbWebhook } from "react-icons/tb";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { WebhookForm } from "@calcom/features/webhooks/components";
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Webhook } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon";
import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
import { Plus, Lock } from "@calcom/ui/components/icon";
export const EventTeamWebhooksTab = ({
eventType,
@@ -85,12 +86,20 @@ export const EventTeamWebhooksTab = ({
<Button
color="secondary"
data-testid="new_webhook"
StartIcon={FiPlus}
StartIcon={Plus}
onClick={() => setCreateModalOpen(true)}>
{t("new_webhook")}
</Button>
);
};
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const webhookLockedStatus = shouldLockDisableProps("webhooks");
return (
<div>
{team && webhooks && !isLoading && (
@@ -98,15 +107,24 @@ export const EventTeamWebhooksTab = ({
<div>
<div>
<>
{isManagedEventType && (
<Alert
severity="neutral"
className="mb-2"
title={t("locked_for_members")}
message={t("locked_webhooks_description")}
/>
)}
{webhooks.length ? (
<>
<div className="mb-8 rounded-md border">
<div className="mb-2 rounded-md border">
{webhooks.map((webhook, index) => {
return (
<WebhookListItem
key={webhook.id}
webhook={webhook}
lastItem={webhooks.length === index + 1}
canEditWebhook={!webhookLockedStatus.disabled}
onEditWebhook={() => {
setEditModalOpen(true);
setWebhookToEdit(webhook);
@@ -122,7 +140,15 @@ export const EventTeamWebhooksTab = ({
Icon={TbWebhook}
headline={t("create_your_first_webhook")}
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
buttonRaw={<NewWebhookButton />}
buttonRaw={
isChildrenManagedEventType && !isManagedEventType ? (
<Button StartIcon={Lock} color="secondary" disabled>
{t("locked_by_admin")}
</Button>
) : (
<NewWebhookButton />
)
}
/>
)}
</>
@@ -1,11 +1,11 @@
export type EventTypeDescriptionSafeProps = {
eventType: { description: string | null };
eventType: { description: string | null; descriptionAsSafeHTML: string | null };
};
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
// @ts-expect-error: @see packages/prisma/middleware/eventTypeDescriptionParseAndSanitize.ts
if (eventType.description) props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML };
if (eventType.description)
props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML || "" };
return <div {...props} />;
};
@@ -1,10 +1,13 @@
import { SchedulingType } from "@prisma/client";
import { Webhook as TbWebhook } from "lucide-react";
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
import { useRouter } from "next/router";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useState, Suspense } from "react";
import type { UseFormReturn } from "react-hook-form";
import { TbWebhook } from "react-icons/tb";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
@@ -32,19 +35,19 @@ import {
VerticalTabs,
} from "@calcom/ui";
import {
FiLink,
FiCalendar,
FiClock,
FiSliders,
FiRepeat,
FiGrid,
FiZap,
FiUsers,
FiExternalLink,
FiCode,
FiTrash,
FiMoreHorizontal,
FiLoader,
Link as LinkIcon,
Calendar,
Clock,
Sliders,
Repeat,
Grid,
Zap,
Users,
ExternalLink,
Code,
Trash,
MoreHorizontal,
Loader,
} from "@calcom/ui/components/icon";
import { EmbedButton, EmbedDialog } from "@components/Embed";
@@ -77,44 +80,38 @@ function getNavigation(props: {
{
name: "event_setup_tab_title",
href: `/event-types/${eventType.id}?tabName=setup`,
icon: FiLink,
icon: LinkIcon,
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
},
{
name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`,
icon: FiCalendar,
info: `default_schedule_name`, // TODO: Get this from props
},
{
name: "event_limit_tab_title",
href: `/event-types/${eventType.id}?tabName=limits`,
icon: FiClock,
icon: Clock,
info: `event_limit_tab_description`,
},
{
name: "event_advanced_tab_title",
href: `/event-types/${eventType.id}?tabName=advanced`,
icon: FiSliders,
icon: Sliders,
info: `event_advanced_tab_description`,
},
{
name: "recurring",
href: `/event-types/${eventType.id}?tabName=recurring`,
icon: FiRepeat,
icon: Repeat,
info: `recurring_event_tab_description`,
},
{
name: "apps",
href: `/event-types/${eventType.id}?tabName=apps`,
icon: FiGrid,
icon: Grid,
//TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
},
{
name: "workflows",
href: `/event-types/${eventType.id}?tabName=workflows`,
icon: FiZap,
icon: Zap,
info: `${enabledWorkflowsNumber} ${t("active")}`,
},
];
@@ -137,7 +134,10 @@ function EventTypeSingleLayout({
const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const hasPermsToDelete = currentUserMembership?.role !== "MEMBER" || !currentUserMembership;
const hasPermsToDelete =
currentUserMembership?.role !== "MEMBER" ||
!currentUserMembership ||
eventType.schedulingType === SchedulingType.MANAGED;
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => {
@@ -157,23 +157,55 @@ function EventTypeSingleLayout({
},
});
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
const navigation = getNavigation({
let navigation = getNavigation({
t,
eventType,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
});
navigation.splice(1, 0, {
name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`,
icon: Calendar,
info:
isManagedEventType || isChildrenManagedEventType
? eventType.schedule === null
? "Member's default schedule"
: isChildrenManagedEventType
? `${
eventType.scheduleName
? `${eventType.scheduleName} - ${t("managed")}`
: `default_schedule_name`
}`
: eventType.scheduleName ?? `default_schedule_name`
: `default_schedule_name`,
});
// If there is a team put this navigation item within the tabs
if (team) {
navigation.splice(2, 0, {
name: "assignment",
href: `/event-types/${eventType.id}?tabName=team`,
icon: FiUsers,
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
icon: Users,
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
isManagedEventType
? ` - ${t("count_members", { count: formMethods.watch("children").length || 0 })}`
: ""
}`,
});
}
if (isManagedEventType || isChildrenManagedEventType) {
// Removing apps and workflows for manageg event types by admins v1
navigation = navigation.slice(0, -2);
} else {
navigation.push({
name: "webhooks",
href: `/event-types/${eventType.id}?tabName=webhooks`,
@@ -189,6 +221,7 @@ function EventTypeSingleLayout({
}`;
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
return (
<Shell
@@ -197,86 +230,96 @@ function EventTypeSingleLayout({
heading={eventType.title}
CTA={
<div className="flex items-center justify-end">
<div className="hidden items-center rounded-md px-2 sm:hover:bg-gray-100 lg:flex">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
{t("hide_from_profile")}
</Skeleton>
<Switch
id="hiddenSwitch"
checked={formMethods.watch("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", e);
}}
/>
</div>
<VerticalDivider className="hidden lg:block" />
{!eventType.metadata.managedEventConfig && (
<>
<div className="sm:hover:bg-subtle hidden items-center rounded-md px-2 lg:flex">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
{t("hide_from_profile")}
</Skeleton>
<Switch
id="hiddenSwitch"
checked={formMethods.watch("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", e);
}}
/>
</div>
<VerticalDivider className="hidden lg:block" />
</>
)}
{/* TODO: Figure out why combined isnt working - works in storybook */}
<ButtonGroup combined containerProps={{ className: "border-gray-300 hidden lg:flex" }}>
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
<Tooltip content={t("preview")}>
<Button
color="secondary"
data-testid="preview-button"
target="_blank"
variant="icon"
href={permalink}
rel="noreferrer"
StartIcon={FiExternalLink}
/>
</Tooltip>
<ButtonGroup combined containerProps={{ className: "border-default hidden lg:flex" }}>
{!isManagedEventType && (
<>
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
<Tooltip content={t("preview")}>
<Button
color="secondary"
data-testid="preview-button"
target="_blank"
variant="icon"
href={permalink}
rel="noreferrer"
StartIcon={ExternalLink}
/>
</Tooltip>
<Button
color="secondary"
variant="icon"
StartIcon={FiLink}
tooltip={t("copy_link")}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
}}
/>
<EmbedButton
embedUrl={encodeURIComponent(embedLink)}
StartIcon={FiCode}
color="secondary"
variant="icon"
tooltip={t("embed")}
/>
<Button
color="secondary"
variant="icon"
StartIcon={FiTrash}
tooltip={t("delete")}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}
/>
<Button
color="secondary"
variant="icon"
StartIcon={LinkIcon}
tooltip={t("copy_link")}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
}}
/>
<EmbedButton
embedUrl={encodeURIComponent(embedLink)}
StartIcon={Code}
color="secondary"
variant="icon"
tooltip={t("embed")}
/>
</>
)}
{!isChildrenManagedEventType && (
<Button
color="destructive"
variant="icon"
StartIcon={Trash}
tooltip={t("delete")}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}
/>
)}
</ButtonGroup>
<VerticalDivider className="hidden lg:block" />
<Dropdown>
<DropdownMenuTrigger asChild>
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
<Button className="lg:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent style={{ minWidth: "200px" }}>
<DropdownMenuItem className="focus:ring-gray-100">
<DropdownMenuItem className="focus:ring-muted">
<DropdownItem
target="_blank"
type="button"
StartIcon={FiExternalLink}
StartIcon={ExternalLink}
href={permalink}
rel="noreferrer">
{t("preview")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100">
<DropdownMenuItem className="focus:ring-muted">
<DropdownItem
type="button"
StartIcon={FiLink}
StartIcon={LinkIcon}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
@@ -284,18 +327,18 @@ function EventTypeSingleLayout({
{t("copy_link")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100">
<DropdownMenuItem className="focus:ring-muted">
<DropdownItem
type="button"
color="destructive"
StartIcon={FiTrash}
StartIcon={Trash}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}>
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuSeparator />
<div className="flex h-9 flex-row items-center justify-between py-2 px-4 sm:hover:bg-gray-100">
<div className="sm:hover:bg-subtle flex h-9 flex-row items-center justify-between py-2 px-4">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
@@ -312,7 +355,7 @@ function EventTypeSingleLayout({
</div>
</DropdownMenuContent>
</Dropdown>
<div className="border-l-2 border-gray-300" />
<div className="border-default border-l-2" />
<Button
className="ml-4 lg:ml-0"
type="submit"
@@ -323,7 +366,7 @@ function EventTypeSingleLayout({
</Button>
</div>
}>
<Suspense fallback={<FiLoader />}>
<Suspense fallback={<Loader />}>
<div className="flex flex-col xl:flex-row xl:space-x-6">
<div className="hidden xl:block">
<VerticalTabs
@@ -339,7 +382,7 @@ function EventTypeSingleLayout({
<div className="w-full ltr:mr-2 rtl:ml-2">
<div
className={classNames(
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
"bg-default border-subtle mt-4 rounded-md sm:mx-0 xl:mt-0",
disableBorder ? "border-0 " : "p-2 md:border md:p-6"
)}>
{children}
@@ -351,14 +394,25 @@ function EventTypeSingleLayout({
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
variety="danger"
title={t("delete_event_type")}
confirmBtnText={t("confirm_delete_event_type")}
loadingText={t("confirm_delete_event_type")}
title={t(`delete${isManagedEvent}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
onConfirm={(e) => {
e.preventDefault();
deleteMutation.mutate({ id: eventType.id });
}}>
{t("delete_event_type_description")}
<p className="mt-5">
<Trans
i18nKey={`delete${isManagedEvent}_event_type_description`}
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
<ul>
<li>Members assigned to this event type will also have their event types deleted.</li>
<li>
Anyone who they&apos;ve shared their link with will no longer be able to book using it.
</li>
</ul>
</Trans>
</p>
</ConfirmationDialogContent>
</Dialog>
<EmbedDialog />
@@ -1,23 +1,26 @@
import type { FormValues } from "pages/event-types/[type]";
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { useFormContext } from "react-hook-form";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Frequency } from "@calcom/prisma/zod-utils";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Alert, Select, SettingsToggle } from "@calcom/ui";
import { Alert, Select, SettingsToggle, TextField } from "@calcom/ui";
type RecurringEventControllerProps = {
recurringEvent: RecurringEvent | null;
eventType: EventTypeSetup;
paymentEnabled: boolean;
};
export default function RecurringEventController({
recurringEvent,
eventType,
paymentEnabled,
}: RecurringEventControllerProps) {
const { t } = useLocale();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(recurringEvent);
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
eventType.recurringEvent
);
const formMethods = useFormContext<FormValues>();
/* Just yearly-0, monthly-1 and weekly-2 */
@@ -28,6 +31,14 @@ export default function RecurringEventController({
value: value.toString(),
}));
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const recurringLocked = shouldLockDisableProps("recurringEvent");
return (
<div className="block items-start sm:flex">
<div className={!paymentEnabled ? "w-full" : ""}>
@@ -37,6 +48,7 @@ export default function RecurringEventController({
<>
<SettingsToggle
title={t("recurring_event")}
{...recurringLocked}
description={t("recurring_event_description")}
checked={recurringEventState !== null}
data-testid="recurring-event-check"
@@ -45,7 +57,7 @@ export default function RecurringEventController({
formMethods.setValue("recurringEvent", null);
setRecurringEventState(null);
} else {
const newVal = recurringEvent || {
const newVal = eventType.recurringEvent || {
interval: 1,
count: 12,
freq: Frequency.WEEKLY,
@@ -57,12 +69,13 @@ export default function RecurringEventController({
{recurringEventState && (
<div data-testid="recurring-event-collapsible" className="text-sm">
<div className="flex items-center">
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
<input
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
<TextField
disabled={recurringLocked.disabled}
type="number"
min="1"
max="20"
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
className="mb-0"
defaultValue={recurringEventState.interval}
onChange={(event) => {
const newVal = {
@@ -77,7 +90,8 @@ export default function RecurringEventController({
options={recurringEventFreqOptions}
value={recurringEventFreqOptions[recurringEventState.freq]}
isSearchable={false}
className="w-18 block h-[36px] min-w-0 rounded-md text-sm"
className="w-18 ml-2 block min-w-0 rounded-md text-sm"
isDisabled={recurringLocked.disabled}
onChange={(event) => {
const newVal = {
...recurringEventState,
@@ -89,13 +103,14 @@ export default function RecurringEventController({
/>
</div>
<div className="mt-4 flex items-center">
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
<input
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
<TextField
disabled={recurringLocked.disabled}
type="number"
min="1"
max="20"
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
defaultValue={recurringEventState.count}
className="mb-0"
onChange={(event) => {
const newVal = {
...recurringEventState,
@@ -105,7 +120,7 @@ export default function RecurringEventController({
setRecurringEventState(newVal);
}}
/>
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">
<p className="text-emphasis ltr:ml-2 rtl:mr-2">
{t("events", {
count: recurringEventState.count,
})}
@@ -1,25 +1,29 @@
import * as RadioGroup from "@radix-ui/react-radio-group";
import type { UnitTypeLongPlural } from "dayjs";
import { Trans } from "next-i18next";
import type { FormValues } from "pages/event-types/[type]";
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import type { Dispatch, SetStateAction } from "react";
import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type z from "zod";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Input, Label, SettingsToggle } from "@calcom/ui";
import { Input, SettingsToggle, RadioField, Select } from "@calcom/ui";
type RequiresConfirmationControllerProps = {
metadata: z.infer<typeof EventTypeMetaDataSchema>;
requiresConfirmation: boolean;
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
seatsEnabled: boolean;
eventType: EventTypeSetup;
};
export default function RequiresConfirmationController({
metadata,
eventType,
requiresConfirmation,
onRequiresConfirmation,
seatsEnabled,
@@ -37,6 +41,23 @@ export default function RequiresConfirmationController({
}
}, [requiresConfirmation]);
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const requiresConfirmationLockedProps = shouldLockDisableProps("requiresConfirmation");
const options = [
{ label: t("minute_timeUnit"), value: "minutes" },
{ label: t("hour_timeUnit"), value: "hours" },
];
const defaultValue = options.find(
(opt) =>
opt.value === (metadata?.requiresConfirmationThreshold?.unit ?? defaultRequiresConfirmationSetup.unit)
);
return (
<div className="block items-start sm:flex">
<div className="w-full">
@@ -46,10 +67,11 @@ export default function RequiresConfirmationController({
render={() => (
<SettingsToggle
title={t("requires_confirmation")}
disabled={seatsEnabled}
disabled={seatsEnabled || requiresConfirmationLockedProps.disabled}
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
description={t("requires_confirmation_description")}
checked={requiresConfirmation}
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
onCheckedChange={(val) => {
formMethods.setValue("requiresConfirmation", val);
onRequiresConfirmation(val);
@@ -77,73 +99,82 @@ export default function RequiresConfirmationController({
);
}
}}>
<div className="-ml-2 flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
<div className="flex items-center">
<RadioGroup.Item
<div className="flex flex-col flex-wrap justify-start gap-y-2">
{(requiresConfirmationSetup === undefined || !requiresConfirmationLockedProps.disabled) && (
<RadioField
label={t("always_requires_confirmation")}
disabled={requiresConfirmationLockedProps.disabled}
id="always"
value="always"
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="always" className="!m-0 flex items-center">
{t("always_requires_confirmation")}
</Label>
</div>
<div className="flex items-center">
<RadioGroup.Item
/>
)}
{(requiresConfirmationSetup !== undefined || !requiresConfirmationLockedProps.disabled) && (
<RadioField
disabled={requiresConfirmationLockedProps.disabled}
className="items-center"
label={
<>
<Trans
i18nKey="when_booked_with_less_than_notice"
defaults="When booked with less than <time></time> notice"
components={{
time: (
<div className="mx-2 inline-flex">
<Input
type="number"
min={1}
disabled={requiresConfirmationLockedProps.disabled}
onChange={(evt) => {
const val = Number(evt.target?.value);
setRequiresConfirmationSetup({
unit:
requiresConfirmationSetup?.unit ??
defaultRequiresConfirmationSetup.unit,
time: val,
});
formMethods.setValue(
"metadata.requiresConfirmationThreshold.time",
val
);
}}
className="border-default !m-0 block w-16 rounded-md text-sm [appearance:textfield]"
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
/>
<label
className={classNames(
requiresConfirmationLockedProps.disabled && "cursor-not-allowed"
)}>
<Select
inputId="notice"
options={options}
isSearchable={false}
isDisabled={requiresConfirmationLockedProps.disabled}
className="ml-2"
onChange={(opt) => {
setRequiresConfirmationSetup({
time:
requiresConfirmationSetup?.time ??
defaultRequiresConfirmationSetup.time,
unit: opt?.value as UnitTypeLongPlural,
});
formMethods.setValue(
"metadata.requiresConfirmationThreshold.unit",
opt?.value as UnitTypeLongPlural
);
}}
defaultValue={defaultValue}
/>
</label>
</div>
),
}}
/>
</>
}
id="notice"
value="notice"
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="notice" className="!m-0 flex items-center">
<Trans
i18nKey="when_booked_with_less_than_notice"
defaults="When booked with less than <time></time> notice"
components={{
time: (
<div className="mx-2 flex">
<Input
type="number"
min={1}
onChange={(evt) => {
const val = Number(evt.target?.value);
setRequiresConfirmationSetup({
unit:
requiresConfirmationSetup?.unit ??
defaultRequiresConfirmationSetup.unit,
time: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
}}
className="!m-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
/>
<select
onChange={(evt) => {
const val = evt.target.value as UnitTypeLongPlural;
setRequiresConfirmationSetup({
time:
requiresConfirmationSetup?.time ??
defaultRequiresConfirmationSetup.time,
unit: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.unit", val);
}}
className="ml-2 block h-9 rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
defaultValue={
metadata?.requiresConfirmationThreshold?.unit ||
defaultRequiresConfirmationSetup.unit
}>
<option value="minutes">{t("minute_timeUnit")}</option>
<option value="hours">{t("hour_timeUnit")}</option>
</select>
</div>
),
}}
/>
</Label>
</div>
/>
)}
</div>
</RadioGroup.Root>
</SettingsToggle>
@@ -1,5 +1,5 @@
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
import { FiClock, FiUser } from "@calcom/ui/components/icon";
import { Clock, User } from "@calcom/ui/components/icon";
function SkeletonLoader() {
return (
@@ -11,7 +11,7 @@ function SkeletonLoader() {
<SkeletonText className="h-4 w-24" />
</div>
</div>
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<ul className="border-subtle bg-default divide-subtle divide-y rounded-md border sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
@@ -32,11 +32,11 @@ function SkeletonItem() {
<div className="">
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
<li className="flex items-center whitespace-nowrap">
<FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<Clock className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
<SkeletonText className="h-4 w-12" />
</li>
<li className="flex items-center whitespace-nowrap">
<FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<User className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
<SkeletonText className="h-4 w-12" />
</li>
</ul>
@@ -6,17 +6,17 @@ import { Button } from "@calcom/ui";
interface ICalendarItem {
title: string;
description?: string;
imageSrc: string;
logo: string;
type: App["type"];
}
const CalendarItem = (props: ICalendarItem) => {
const { title, imageSrc, type } = props;
const { title, logo, type } = props;
const { t } = useLocale();
return (
<div className="flex flex-row items-center justify-between p-5">
<div className="flex items-center space-x-3">
<img src={imageSrc} alt={title} className="h-8 w-8" />
<img src={logo} alt={title} className="h-8 w-8" />
<p className="text-sm font-bold">{title}</p>
</div>
<InstallAppButtonWithoutPlanCheck
@@ -28,14 +28,14 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
<p className="font-sans text-sm font-bold leading-5">
{name}
{/* Temporarily removed till we use it on another place */}
{/* <span className="mx-1 rounded-[4px] bg-green-100 py-[2px] px-[6px] font-sans text-xs font-medium text-green-600">
{/* <span className="mx-1 rounded-[4px] bg-success py-[2px] px-[6px] font-sans text-xs font-medium text-green-600">
{t("default")}
</span> */}
</p>
<div className="fle-row flex">
<span
title={externalId}
className="max-w-44 mt-1 overflow-hidden text-ellipsis whitespace-nowrap font-sans text-sm text-gray-500">
className="max-w-44 text-subtle mt-1 overflow-hidden text-ellipsis whitespace-nowrap font-sans text-sm">
{externalId}{" "}
</span>
</div>
@@ -44,11 +44,11 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
{/* <Button
color="minimal"
type="button"
className="ml-auto flex rounded-md border border-gray-200 py-[10x] px-4 font-sans text-sm">
className="ml-auto flex rounded-md border border-subtle py-[10x] px-4 font-sans text-sm">
{t("edit")}
</Button> */}
</div>
<div className="h-[1px] w-full border-b border-gray-200" />
<div className="border-subtle h-[1px] w-full border-b" />
<div>
<ul className="p-4">
{calendars?.map((calendar, i) => (
@@ -1,9 +1,8 @@
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterInputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
interface ICreateEventsOnCalendarSelectProps {
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
}
@@ -17,7 +16,7 @@ const CreateEventsOnCalendarSelect = (props: ICreateEventsOnCalendarSelectProps)
<>
<div className="mt-6 flex flex-row">
<div className="w-full">
<label htmlFor="createEventsOn" className="flex text-sm font-medium text-gray-700">
<label htmlFor="createEventsOn" className="text-default flex text-sm font-medium">
{t("create_events_on")}
</label>
<div className="mt-2">
@@ -31,13 +31,13 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
firstCalendar &&
firstCalendar.integration &&
firstCalendar.integration.title &&
firstCalendar.integration.imageSrc && (
firstCalendar.integration.logo && (
<>
<List className="rounded-md border border-gray-200 bg-white p-0 dark:bg-black">
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black">
<ConnectedCalendarItem
key={firstCalendar.integration.title}
name={firstCalendar.integration.title}
logo={firstCalendar.integration.imageSrc}
logo={firstCalendar.integration.logo}
externalId={
firstCalendar && firstCalendar.calendars && firstCalendar.calendars.length > 0
? firstCalendar.calendars[0].externalId
@@ -49,22 +49,22 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
</List>
{/* Create event on selected calendar */}
<CreateEventsOnCalendarSelect calendar={destinationCalendar} />
<p className="mt-4 text-sm text-gray-500">{t("connect_calendars_from_app_store")}</p>
<p className="text-subtle mt-4 text-sm">{t("connect_calendars_from_app_store")}</p>
</>
)}
{/* Connect calendars list */}
{firstCalendar === undefined && queryIntegrations.data && queryIntegrations.data.items.length > 0 && (
<List className="mx-1 divide-y divide-gray-200 rounded-md border border-gray-200 bg-white p-0 dark:bg-black sm:mx-0">
<List className="bg-default divide-subtle border-subtle mx-1 divide-y rounded-md border p-0 dark:bg-black sm:mx-0">
{queryIntegrations.data &&
queryIntegrations.data.items.map((item) => (
<li key={item.title}>
{item.title && item.imageSrc && (
{item.title && item.logo && (
<CalendarItem
type={item.type}
title={item.title}
description={item.description}
imageSrc={item.imageSrc}
logo={item.logo}
/>
)}
</li>
@@ -73,7 +73,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
)}
{queryIntegrations.isLoading && (
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white p-0 dark:bg-black">
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border p-0 dark:bg-black">
{[0, 0, 0, 0].map((_item, index) => {
return (
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
@@ -89,7 +89,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
type="button"
data-testid="save-calendar-button"
className={classNames(
"mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm text-white",
"text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm",
disabledNextButton ? "cursor-not-allowed opacity-20" : ""
)}
onClick={() => nextStep()}
@@ -50,7 +50,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions);
return (
<Form
className="w-full bg-white text-black dark:bg-opacity-5 dark:text-white"
className="bg-default dark:text-inverted text-emphasis w-full dark:bg-opacity-5"
form={availabilityForm}
handleSubmit={async (values) => {
try {
@@ -117,7 +117,7 @@ const UserProfile = (props: IUserProfileProps) => {
name="avatar"
id="avatar"
placeholder="URL"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm focus:border-gray-800 focus:outline-none focus:ring-gray-800"
className="border-default focus:ring-empthasis mt-1 block w-full rounded-sm border px-3 py-2 text-sm focus:border-gray-800 focus:outline-none"
defaultValue={imageSrc}
/>
<div className="flex items-center px-4">
@@ -142,19 +142,19 @@ const UserProfile = (props: IUserProfileProps) => {
</div>
</div>
<fieldset className="mt-8">
<Label className="mb-2 block text-sm font-medium text-gray-700">{t("about")}</Label>
<Label className="text-default mb-2 block text-sm font-medium">{t("about")}</Label>
<Editor
getText={() => md.render(getValues("bio") || user?.bio || "")}
setText={(value: string) => setValue("bio", turndown(value))}
excludedToolbarItems={["blockType"]}
/>
<p className="mt-2 font-sans text-sm font-normal text-gray-600 dark:text-white">
<p className="dark:text-inverted text-default mt-2 font-sans text-sm font-normal">
{t("few_sentences_about_yourself")}
</p>
</fieldset>
<Button
type="submit"
className="mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm text-white">
className="text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm">
{t("finish")}
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</Button>
@@ -1,8 +1,11 @@
import { ArrowRightIcon } from "@heroicons/react/outline";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { trpc } from "@calcom/trpc/react";
@@ -22,23 +25,30 @@ const UserSettings = (props: IUserSettingsProps) => {
const { t } = useLocale();
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
const telemetry = useTelemetry();
const userSettingsSchema = z.object({
name: z
.string()
.min(1)
.max(FULL_NAME_LENGTH_MAX_LIMIT, {
message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
}),
});
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
} = useForm<z.infer<typeof userSettingsSchema>>({
defaultValues: {
name: user?.name || "",
},
reValidateMode: "onChange",
resolver: zodResolver(userSettingsSchema),
});
useEffect(() => {
telemetry.event(telemetryEventTypes.onboardingStarted);
}, [telemetry]);
const defaultOptions = { required: true, maxLength: 255 };
const utils = trpc.useContext();
const onSuccess = async () => {
await utils.viewer.me.invalidate();
@@ -63,27 +73,29 @@ const UserSettings = (props: IUserSettingsProps) => {
{/* Full name textfield */}
<div className="w-full">
<label htmlFor="name" className="mb-2 block text-sm font-medium text-gray-700">
<label htmlFor="name" className="text-default mb-2 block text-sm font-medium">
{t("full_name")}
</label>
<input
{...register("name", defaultOptions)}
{...register("name", {
required: true,
})}
id="name"
name="name"
type="text"
autoComplete="off"
autoCorrect="off"
className="w-full rounded-md border border-gray-300 text-sm"
className="border-default w-full rounded-md border text-sm"
/>
{errors.name && (
<p data-testid="required" className="py-2 text-xs text-red-500">
{t("required")}
{errors.name.message}
</p>
)}
</div>
{/* Timezone select field */}
<div className="w-full">
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
<label htmlFor="timeZone" className="text-default block text-sm font-medium">
{t("timezone")}
</label>
@@ -94,7 +106,7 @@ const UserSettings = (props: IUserSettingsProps) => {
className="mt-2 w-full rounded-md text-sm"
/>
<p className="mt-3 flex flex-row font-sans text-xs leading-tight text-gray-500 dark:text-white">
<p className="text-subtle dark:text-inverted mt-3 flex flex-row font-sans text-xs leading-tight">
{t("current_time")} {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
</p>
</div>
+4 -2
View File
@@ -24,7 +24,9 @@ export default function WizardLayout({
}, [router.asPath]);
return (
<div className="dark:bg-brand dark:text-brand-contrast min-h-screen text-black" data-testid="onboarding">
<div
className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen"
data-testid="onboarding">
<div>
<Toaster position="bottom-right" />
</div>
@@ -34,7 +36,7 @@ export default function WizardLayout({
<div className="mx-auto sm:max-w-[520px]">
<header>
<p className="font-cal mb-3 text-[28px] font-medium leading-7">{title}&nbsp;</p>
<p className="font-sans text-sm font-normal text-gray-500">{subtitle}&nbsp;</p>
<p className="text-subtle font-sans text-sm font-normal">{subtitle}&nbsp;</p>
</header>
<Steps maxSteps={maxSteps} currentStep={currentStep} navigateToStep={noop} />
</div>
@@ -59,14 +59,14 @@ const ChangePasswordSection = () => {
return (
<>
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
<form className="divide-subtle divide-y lg:col-span-9" onSubmit={changePasswordHandler}>
<div className="py-6 lg:pb-5">
<div className="my-3">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("change_password")}</h2>
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("change_password")}</h2>
</div>
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0">
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-1/2">
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
<label htmlFor="current_password" className="text-default block text-sm font-medium">
{t("current_password")}
</label>
<div className="mt-1">
@@ -77,13 +77,13 @@ const ChangePasswordSection = () => {
name="current_password"
id="current_password"
required
className="block w-full rounded-sm border-gray-300 text-sm"
className="border-default block w-full rounded-sm text-sm"
placeholder={t("your_old_password")}
/>
</div>
</div>
<div className="w-full sm:w-1/2">
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">
<label htmlFor="new_password" className="text-default block text-sm font-medium">
{t("new_password")}
</label>
<div className="mt-1">
@@ -94,7 +94,7 @@ const ChangePasswordSection = () => {
value={newPassword}
required
onInput={(e) => setNewPassword(e.currentTarget.value)}
className="block w-full rounded-sm border-gray-300 text-sm"
className="border-default block w-full rounded-sm text-sm"
placeholder={t("super_secure_new_password")}
/>
</div>
@@ -70,10 +70,10 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
<div className="mb-4">
<PasswordField
labelProps={{
className: "block text-sm font-medium text-gray-700",
className: "block text-sm font-medium text-default",
}}
{...form.register("password")}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
/>
<Label className="mt-4"> {t("2fa_code")}</Label>
@@ -82,7 +82,7 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
<Button type="submit" className="ms-2 me-2" disabled={isDisabling}>
{t("disable")}
</Button>
<Button color="secondary" onClick={onCancel}>
@@ -22,14 +22,14 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
<div>
<div className="flex flex-row items-center">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
{t("user_impersonation_heading")}
</h2>
<Badge className="ml-2 text-xs" variant={!disableImpersonation ? "success" : "gray"}>
{!disableImpersonation ? t("enabled") : t("disabled")}
</Badge>
</div>
<p className="mt-1 text-sm text-gray-500">{t("user_impersonation_description")}</p>
<p className="text-subtle mt-1 text-sm">{t("user_impersonation_description")}</p>
</div>
<div className="mt-5 sm:mt-0 sm:self-center">
<Button
@@ -135,7 +135,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<form onSubmit={handleSetup}>
<div className="mb-4">
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
<label htmlFor="password" className="text-default mt-4 block text-sm font-medium">
{t("password")}
</label>
<div className="mt-1">
@@ -146,7 +146,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
required
value={password}
onInput={(e) => setPassword(e.currentTarget.value)}
className="block w-full rounded-sm border-gray-300 text-sm"
className="border-default block w-full rounded-sm text-sm"
/>
</div>
@@ -178,22 +178,19 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<Button
type="submit"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
className="ms-2 me-2"
onClick={handleSetup}
disabled={password.length === 0 || isSubmitting}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.DisplayQrCode} current={step}>
<Button
type="submit"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
onClick={() => setStep(SetupStep.EnterTotpCode)}>
<Button type="submit" className="ms-2 me-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isSubmitting}>
<Button type="submit" className="ms-2 me-2" disabled={isSubmitting}>
{t("enable")}
</Button>
</WithStep>
@@ -17,12 +17,12 @@ const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
<div>
<div className="flex flex-row items-center">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("2fa")}</h2>
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("2fa")}</h2>
<Badge className="ml-2 text-xs" variant={enabled ? "success" : "gray"}>
{enabled ? t("enabled") : t("disabled")}
</Badge>
</div>
<p className="mt-1 text-sm text-gray-500">{t("add_an_extra_layer_of_security")}</p>
<p className="text-subtle mt-1 text-sm">{t("add_an_extra_layer_of_security")}</p>
</div>
<div className="mt-5 sm:mt-0 sm:self-center">
<Button
@@ -1,16 +1,16 @@
import { FiShield } from "@calcom/ui/components/icon";
import { Shield } from "@calcom/ui/components/icon";
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
return (
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<FiShield className="h-6 w-6 text-white" />
<Shield className="text-inverted h-6 w-6" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
{title}
</h3>
<p className="text-sm text-gray-400">{description}</p>
<p className="text-muted text-sm">{description}</p>
</div>
</div>
);
@@ -3,7 +3,7 @@ import { useMutation } from "@tanstack/react-query";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Badge, showToast, Switch } from "@calcom/ui";
import { FiArrowLeft } from "@calcom/ui/components/icon";
import { ArrowLeft } from "@calcom/ui/components/icon";
export function CalendarSwitch(props: {
type: string;
@@ -75,7 +75,9 @@ export function CalendarSwitch(props: {
/>
{props.defaultSelected && (
<Badge variant="gray">
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
<div className="flex items-center">
<ArrowLeft className="text-default mr-1 h-4 w-4" /> {t("adding_events_to")}
</div>
</Badge>
)}
</div>
@@ -77,10 +77,10 @@ const DisableTwoFactorAuthModal = ({
<div className="mb-4">
<PasswordField
labelProps={{
className: "block text-sm font-medium text-gray-700",
className: "block text-sm font-medium text-default",
}}
{...form.register("password")}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
/>
<Label className="mt-4"> {t("2fa_code")}</Label>
@@ -90,7 +90,7 @@ const DisableTwoFactorAuthModal = ({
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
<Button type="submit" className="ms-2 me-2" disabled={isDisabling}>
{t("disable")}
</Button>
<Button color="secondary" onClick={onCancel}>

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