Compare commits

...
203 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
714 changed files with 16021 additions and 8586 deletions

No files matched your search

+2 -3
View File
@@ -160,9 +160,6 @@ CLOSECOM_API_KEY=
# Sendgrid internal sync service # Sendgrid internal sync service
SENDGRID_SYNC_API_KEY= SENDGRID_SYNC_API_KEY=
# Sentry
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_IGNORE_API_RESOLUTION_ERROR=
# Change your Brand # Change your Brand
NEXT_PUBLIC_APP_NAME="Cal.com" NEXT_PUBLIC_APP_NAME="Cal.com"
@@ -177,3 +174,5 @@ CSP_POLICY=
# Vercel Edge Config # Vercel Edge Config
EDGE_CONFIG= EDGE_CONFIG=
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
+2 -1
View File
@@ -5,7 +5,8 @@
# the repo. Unless a later match takes precedence, # the repo. Unless a later match takes precedence,
# @global-owner1 and @global-owner2 will be requested for # @global-owner1 and @global-owner2 will be requested for
# review when someone opens a pull request. # review when someone opens a pull request.
* @calcom/reviewers # ⬇️ Removed so we can manually trigger checks
# * @calcom/reviewers
.github/ @calcom/core .github/ @calcom/core
.husky/ @calcom/core .husky/ @calcom/core
@@ -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
+6 -3
View File
@@ -15,6 +15,9 @@ jobs:
steps: steps:
- uses: actions/stale@v7 - uses: actions/stale@v7
with: with:
days-before-stale: 60 days-before-issue-stale: 60
include-only-assigned: true days-before-issue-close: -1
days-before-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."
+3 -1
View File
@@ -15,6 +15,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: crowdin action - name: crowdin action
uses: crowdin/github-action@1.5.1 uses: crowdin/github-action@1.5.1
@@ -26,6 +28,6 @@ jobs:
localization_branch_name: main localization_branch_name: main
create_pull_request: false create_pull_request: false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
@@ -0,0 +1,149 @@
const https = require("https");
async function applyLabelFromLinkedIssueToPR(pr, token) {
// Get the labels from issues linked to the PR
const query = `
query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
closingIssuesReferences(first: 10) {
nodes {
number
labels(first: 10) {
nodes {
name
}
}
}
}
}
}
}
`;
const graphqlData = JSON.stringify({
query,
variables: {
owner: pr.base.repo.owner.login,
repo: pr.base.repo.name,
prNumber: pr.number,
},
});
const requestOptions = {
hostname: "api.github.com",
path: "/graphql",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": graphqlData.length,
"Authorization": "Bearer " + token,
"User-Agent": "Node.js",
},
};
// Use the native Node.js request library to make the request
let linkedIssues;
linkedIssues = await new Promise((resolve, reject) => {
const request = https.request(requestOptions, (response) => {
let responseBody = "";
response.on("data", (chunk) => {
responseBody += chunk;
});
response.on("end", () => {
resolve(
JSON.parse(responseBody)
?.data
?.repository
?.pullRequest
?.closingIssuesReferences
?.nodes
);
});
});
request.on("error", (error) => {
reject(error);
});
request.write(graphqlData);
request.end();
});
if (!linkedIssues || linkedIssues.length === 0) {
console.log("No issue linked.");
return;
}
// Iterate over linked issues and apply labels to PR
for (const issue of linkedIssues) {
const labels = issue?.labels?.nodes?.map((label) => label.name);
if (!labels || labels.length === 0) {
console.log(`No labels found on the linked issue #${issue.number}.`);
continue;
}
const labelsData = JSON.stringify({
labels: labels,
});
const requestOptions = {
hostname: "api.github.com",
path: `/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/issues/${pr.number}/labels`,
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": labelsData.length,
"Authorization": "Bearer " + token,
"User-Agent": "Node.js",
},
};
let labelResult;
labelResult = await new Promise((resolve, reject) => {
const request = https.request(requestOptions, (response) => {
let responseBody = "";
response.on("data", (chunk) => {
responseBody += chunk;
});
response.on("data", () => {});
response.on("end", () => {
resolve(JSON.parse(responseBody));
});
});
request.on("error", (error) => {
reject(error);
});
request.write(labelsData);
request.end();
});
if (labelResult.message) {
console.log(`Error labelling PR: ${labelResult.message}`);
continue;
}
console.log(
`Applied labels: ${labels.join(", ")} to PR #${
pr.number
} from linked issue #${issue.number}`
);
}
}
// Pass the PR data and GitHub token to the script
(async () => {
if (!process.env.PR_DATA) {
console.log("No PR data found.");
return;
}
const prData = JSON.parse(process.env.PR_DATA);
const token = process.env.GITHUB_TOKEN;
await applyLabelFromLinkedIssueToPR(prData, token);
})();
+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"; import { classNames } from "@calcom/lib";
@@ -42,7 +43,7 @@ export function VariantsTable({
</table> </table>
</div> </div>
{!isDark && ( {!isDark && (
<div data-mode="dark"> <div data-mode="dark" className="dark">
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}> <VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
{children} {children}
</VariantsTable> </VariantsTable>
@@ -193,3 +193,88 @@
.docs-story { .docs-story {
padding: 24px !important; 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;
}
}
-2
View File
@@ -68,5 +68,3 @@ public/embed
# Copied app-store images # Copied app-store images
public/app-store public/app-store
# Sentry
.sentryclirc
-53
View File
@@ -103,59 +103,6 @@ paths:
summary: Reset a user's password summary: Reset a user's password
tags: tags:
- Authentication - 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: /api/availability/calendar:
get: get:
description: Gets the user's selected calendars. description: Gets the user's selected calendars.
+3 -3
View File
@@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale"; 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() { export default function AddToHomescreen() {
const { t } = useLocale(); 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" /> <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> </svg>
</span> </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> <span className="inline">{t("add_to_homescreen")}</span>
</p> </p>
</div> </div>
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
type="button" type="button"
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white"> 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> <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> </button>
</div> </div>
</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 { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { Badge, ListItemText } from "@calcom/ui"; 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 }; type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
@@ -63,12 +63,12 @@ export default function AppListCard(props: AppListCardProps) {
}, []); }, []);
return ( 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"> <div className="flex gap-x-3 px-5 py-4">
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null} {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 grow flex-col gap-y-1 truncate">
<div className="flex items-center gap-x-2"> <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"> <div className="flex items-center gap-x-2">
{isDefault && <Badge variant="green">{t("default")}</Badge>} {isDefault && <Badge variant="green">{t("default")}</Badge>}
{isTemplate && <Badge variant="red">Template</Badge>} {isTemplate && <Badge variant="red">Template</Badge>}
@@ -77,7 +77,7 @@ export default function AppListCard(props: AppListCardProps) {
<ListItemText component="p">{description}</ListItemText> <ListItemText component="p">{description}</ListItemText>
{invalidCredential && ( {invalidCredential && (
<div className="flex gap-x-2 pt-2"> <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"> <ListItemText component="p" className="whitespace-pre-wrap text-red-500">
{t("invalid_credential")} {t("invalid_credential")}
</ListItemText> </ListItemText>
@@ -1,137 +0,0 @@
import classNames from "classnames";
import React, { useEffect, useState } from "react";
import Select from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { DestinationCalendar } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
interface Props {
onChange: (value: { externalId: string; integration: string }) => void;
isLoading?: boolean;
hidePlaceholder?: boolean;
/** The external Id of the connected calendar */
value: string | undefined;
destinationCalendar?: DestinationCalendar | null;
}
const DestinationCalendarSelector = ({
onChange,
isLoading,
value,
hidePlaceholder,
destinationCalendar,
}: Props): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.connectedCalendars.useQuery();
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
// Extra styles to show prefixed text in react-select
const content = (hidePlaceholder = false) => {
if (!hidePlaceholder) {
return {
alignItems: "center",
display: "flex",
":before": {
content: `'${t("select_destination_calendar")}:'`,
display: "block",
marginRight: 8,
},
};
}
return {};
};
useEffect(() => {
const selected = query.data?.connectedCalendars
.map((connected) => connected.calendars ?? [])
.flat()
.find((cal) => cal.externalId === value);
if (selected) {
setSelectedOption({
value: `${selected.integration}:${selected.externalId}`,
label: selected.name || "",
});
}
}, [query.data?.connectedCalendars, value]);
if (!query.data?.connectedCalendars.length) {
return null;
}
const options =
query.data.connectedCalendars.map((selectedCalendar) => ({
key: selectedCalendar.credentialId,
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
options: (selectedCalendar.calendars ?? [])
.filter((cal) => cal.readOnly === false)
.map((cal) => ({
label: cal.name || "",
value: `${cal.integration}:${cal.externalId}`,
})),
})) ?? [];
const defaultCalendarSelectedString = destinationCalendar?.externalId
? `(${
destinationCalendar.externalId.length > 15
? destinationCalendar.externalId.substring(0, 15) + "..."
: destinationCalendar.externalId
})`
: "";
return (
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
<Select
name="primarySelectedCalendar"
placeholder={
!hidePlaceholder ? (
`${t("select_destination_calendar")}`
) : (
<span className="whitespace-nowrap">
{t("default_calendar_selected")} {defaultCalendarSelectedString}
</span>
)
}
options={options}
styles={{
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "6px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
width: "100%",
},
};
},
}}
isSearchable={false}
className={classNames(
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
!hidePlaceholder && "font-medium"
)}
onChange={(option) => {
setSelectedOption(option);
if (!option) {
return;
}
/* Split only the first `:`, since Apple uses the full URL as externalId */
const [integration, externalId] = option.value.split(/:(.+)/);
onChange({
integration,
externalId,
});
}}
isLoading={isLoading}
value={selectedOption}
components={{
IndicatorSeparator: () => null,
}}
/>
</div>
);
};
export default DestinationCalendarSelector;
+105 -108
View File
@@ -1,4 +1,4 @@
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
import classNames from "classnames"; import classNames from "classnames";
import type { NextRouter } from "next/router"; import type { NextRouter } from "next/router";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -22,7 +22,7 @@ import {
TextField, TextField,
ColorPicker, ColorPicker,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon"; import { Code, Trello, Sun, ArrowLeft } from "@calcom/ui/components/icon";
import Select from "@components/ui/form/Select"; import Select from "@components/ui/form/Select";
@@ -313,7 +313,7 @@ const embeds: {
}[] = [ }[] = [
{ {
title: "Inline Embed", title: "Inline Embed",
subtitle: "Loads your Cal scheduling page directly inline with your other website content", subtitle: "Loads your event type directly inline with your other website content.",
type: "inline", type: "inline",
illustration: ( illustration: (
<svg <svg
@@ -493,7 +493,7 @@ const tabs = [
{ {
name: "HTML", name: "HTML",
href: "embedTabName=embed-code", href: "embedTabName=embed-code",
icon: FiCode, icon: Code,
type: "code", type: "code",
Component: forwardRef< Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null, HTMLTextAreaElement | HTMLIFrameElement | null,
@@ -509,7 +509,7 @@ const tabs = [
return ( return (
<> <>
<div> <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 })} {t("place_where_cal_widget_appear", { appName: APP_NAME })}
</small> </small>
</div> </div>
@@ -517,7 +517,7 @@ const tabs = [
data-testid="embed-code" data-testid="embed-code"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>} ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name="embed-code" 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" }} style={{ resize: "none", overflow: "auto" }}
readOnly readOnly
value={ value={
@@ -534,7 +534,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
<!-- Cal ${embedType} embed code ends -->` <!-- Cal ${embedType} embed code ends -->`
} }
/> />
<p className="hidden text-sm text-gray-500"> <p className="text-subtle hidden text-sm">
{t( {t(
"Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options." "Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options."
)} )}
@@ -546,7 +546,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
{ {
name: "React", name: "React",
href: "embedTabName=embed-react", href: "embedTabName=embed-react",
icon: FiCode, icon: Code,
type: "code", type: "code",
Component: forwardRef< Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null, HTMLTextAreaElement | HTMLIFrameElement | null,
@@ -561,12 +561,12 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
} }
return ( 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 <TextArea
data-testid="embed-react" data-testid="embed-react"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>} ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name="embed-react" 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 readOnly
style={{ resize: "none", overflow: "auto" }} style={{ resize: "none", overflow: "auto" }}
value={`/* First make sure that you have installed the package */ value={`/* First make sure that you have installed the package */
@@ -586,7 +586,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
{ {
name: "Preview", name: "Preview",
href: "embedTabName=embed-preview", href: "embedTabName=embed-preview",
icon: FiTrello, icon: Trello,
type: "iframe", type: "iframe",
Component: forwardRef< Component: forwardRef<
HTMLIFrameElement | HTMLTextAreaElement | null, HTMLIFrameElement | HTMLTextAreaElement | null,
@@ -622,7 +622,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => { const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
return ( return (
<components.Control {...props}> <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} {children}
</components.Control> </components.Control>
); );
@@ -634,17 +634,17 @@ const ChooseEmbedTypesDialogContent = () => {
return ( return (
<DialogContent className="rounded-lg p-10" type="creation" size="lg"> <DialogContent className="rounded-lg p-10" type="creation" size="lg">
<div className="mb-2"> <div className="mb-2">
<h3 className="mb-2 text-2xl font-bold leading-none text-gray-900" id="modal-title"> <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 })} {t("how_you_want_add_cal_site", { appName: APP_NAME })}
</h3> </h3>
<div> <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> </div>
<div className="flex items-start"> <div className="items-start space-y-2 md:flex md:space-y-0">
{embeds.map((embed, index) => ( {embeds.map((embed, index) => (
<button <button
className="hover:border-primary-900 w-1/3 rounded-md border border-gray-200 bg-gray-50 p-6 text-left hover:rounded-md hover:bg-gray-100 ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0" 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} key={index}
data-testid={embed.type} data-testid={embed.type}
onClick={() => { onClick={() => {
@@ -652,11 +652,11 @@ const ChooseEmbedTypesDialogContent = () => {
embedType: embed.type, embedType: embed.type,
}); });
}}> }}>
<div className="order-none box-border flex-none rounded-md 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} {embed.illustration}
</div> </div>
<div className="mt-4 font-semibold leading-none text-gray-900">{embed.title}</div> <div className="text-emphasis mt-4 font-semibold">{embed.title}</div>
<p className="mt-2 text-sm text-gray-500">{embed.subtitle}</p> <p className="text-subtle mt-2 text-sm">{embed.subtitle}</p>
</button> </button>
))} ))}
</div> </div>
@@ -798,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
} }
const ThemeOptions = [ const ThemeOptions = [
{ value: Theme.auto, label: "Auto Theme" }, { value: Theme.auto, label: "Auto" },
{ value: Theme.dark, label: "Dark Theme" }, { value: Theme.dark, label: "Dark Theme" },
{ value: Theme.light, label: "Light Theme" }, { value: Theme.light, label: "Light Theme" },
]; ];
@@ -815,67 +815,62 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
]; ];
return ( return (
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation"> <DialogContent
ref={dialogContentRef}
className="rounded-lg p-0.5 sm:max-w-[80rem]"
enableOverflow
type="creation">
<div className="flex"> <div className="flex">
<div className="flex w-1/3 flex-col bg-gray-50 p-8"> <div className="bg-muted flex w-1/3 flex-col p-8">
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title"> <h3
className="text-emphasis mb-2.5 flex items-center text-xl font-semibold leading-5"
id="modal-title">
<button <button
className="h-6 w-6"
onClick={() => { onClick={() => {
removeQueryParams(router, ["embedType", "embedTabName"]); removeQueryParams(router, ["embedType", "embedTabName"]);
}}> }}>
<FiArrowLeft className="mr-4 w-4" /> <ArrowLeft className="mr-4 w-4" />
</button> </button>
{embed.title} {embed.title}
</h3> </h3>
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} /> <h4 className="text-emphasis mb-6 text-sm font-normal">{embed.subtitle}</h4>
<div className="flex flex-col overflow-y-auto"> <div className="flex flex-col">
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}> <div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible <Collapsible
open={isEmbedCustomizationOpen} open={isEmbedCustomizationOpen}
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}> onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
<CollapsibleTrigger
type="button"
className="flex w-full items-center text-base font-medium text-gray-900">
<div>
{embedType === "inline"
? "Inline Embed Customization"
: embedType === "floating-popup"
? "Floating Popup Customization"
: "Element Click Customization"}
</div>
<FiChevronRight
className={`${
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-gray-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent className="text-sm"> <CollapsibleContent className="text-sm">
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}> <div className={classNames(embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */} {/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-sm">Embed Window Sizing</div> <div className="text-default mb-[9px] text-sm">Window sizing</div>
<div className="justify-left flex items-center"> <div className="justify-left flex items-center !font-normal">
<TextField <div className="mr-[9px]">
labelProps={{ className: "hidden" }} <TextField
required labelProps={{ className: "hidden" }}
value={previewState.inline.width} className="focus:ring-offset-0"
onChange={(e) => { required
setPreviewState((previewState) => { value={previewState.inline.width}
const width = e.target.value || "100%"; onChange={(e) => {
setPreviewState((previewState) => {
const width = e.target.value || "100%";
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<>W</>}
/>
</div>
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<>W</>}
/>
<span className="p-2">×</span>
<TextField <TextField
labelProps={{ className: "hidden" }} labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
value={previewState.inline.height} value={previewState.inline.height}
required required
onChange={(e) => { onChange={(e) => {
@@ -898,7 +893,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div <div
className={classNames( className={classNames(
"mt-4 items-center justify-between", "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> <div className="mb-2 text-sm">Button Text</div>
{/* Default Values should come from preview iframe */} {/* Default Values should come from preview iframe */}
@@ -922,7 +917,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div <div
className={classNames( className={classNames(
"mt-4 flex items-center justify-start", "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 <Switch
defaultChecked={true} defaultChecked={true}
@@ -938,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>
<div <div
className={classNames( className={classNames(
"mt-4 items-center justify-between", "mt-4 items-center justify-between",
embedType === "floating-popup" ? "" : "hidden" embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}> )}>
<div className="mb-2">Position of Button</div> <div className="mb-2">Position of Button</div>
<Select <Select
@@ -962,7 +959,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
options={FloatingPopupPositionOptions} options={FloatingPopupPositionOptions}
/> />
</div> </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>Button Color</div>
<div className="w-full"> <div className="w-full">
<ColorPicker <ColorPicker
@@ -983,7 +984,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
/> />
</div> </div>
</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>Text Color</div>
<div className="w-full"> <div className="w-full">
<ColorPicker <ColorPicker
@@ -1007,22 +1012,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
</div> </div>
<hr className="mt-4" /> <div className="font-medium">
<div className="mt-4 font-medium">
<Collapsible <Collapsible
open={isBookingCustomizationOpen} open={isBookingCustomizationOpen}
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}> onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleTrigger className="flex w-full" type="button">
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
<FiChevronRight
className={`${
isBookingCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-gray-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="mt-6 text-sm"> <div className="mt-6 text-sm">
<div className="mb-4 flex items-center justify-start space-x-2 rtl:space-x-reverse"> <div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
<Switch <Switch
checked={previewState.hideEventTypeDetails} checked={previewState.hideEventTypeDetails}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
@@ -1034,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> </div>
<Label className=""> {[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="mb-6">
<div className="mb-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
<Label>
<div className="mb-2">Theme</div> <div className="mb-2">Theme</div>
<Select <Select
className="w-full" className="w-full"
defaultValue={ThemeOptions[0]} defaultValue={ThemeOptions[0]}
components={{ components={{
Control: ThemeSelectControl, Control: ThemeSelectControl,
IndicatorSeparator: () => null,
}} }}
onChange={(option) => { onChange={(option) => {
if (!option) { if (!option) {
@@ -1058,30 +1079,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
options={ThemeOptions} options={ThemeOptions}
/> />
</Label> </Label>
{[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="pb-4">
<div className="mb-2 pt-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
+4 -4
View File
@@ -123,7 +123,7 @@ export default function ImageUploader({
<DialogContent> <DialogContent>
<div className="mb-4 sm:flex sm:items-start"> <div className="mb-4 sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:text-left"> <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 })} {t("upload_target", { target })}
</h3> </h3>
</div> </div>
@@ -131,9 +131,9 @@ export default function ImageUploader({
<div className="mb-4"> <div className="mb-4">
<div className="cropper mt-6 flex flex-col items-center justify-center p-8"> <div className="cropper mt-6 flex flex-col items-center justify-center p-8">
{!result && ( {!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 && ( {!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 })} {t("no_target", { target })}
</p> </p>
)} )}
@@ -144,7 +144,7 @@ export default function ImageUploader({
</div> </div>
)} )}
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />} {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 <input
onInput={onInputFile} onInput={onInputFile}
type="file" type="file"
+4 -4
View File
@@ -69,8 +69,8 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
onClick={onClick} onClick={onClick}
className={classNames( className={classNames(
isCurrent isCurrent
? "border-gray-900 text-gray-900" ? "text-emphasis border-gray-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700", : "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", "group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
className className
)} )}
@@ -78,7 +78,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
{tab.icon && ( {tab.icon && (
<tab.icon <tab.icon
className={classNames( 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" "-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
)} )}
aria-hidden="true" aria-hidden="true"
@@ -91,7 +91,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
); );
})} })}
</nav> </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 Shell from "@calcom/features/shell/Shell";
import { ErrorBoundary } from "@calcom/ui"; 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"; import NavTabs from "./NavTabs";
@@ -11,32 +11,32 @@ const tabs = [
{ {
name: "profile", name: "profile",
href: "/settings/my-account/profile", href: "/settings/my-account/profile",
icon: FiUser, icon: User,
}, },
{ {
name: "teams", name: "teams",
href: "/settings/teams", href: "/settings/teams",
icon: FiUsers, icon: Users,
}, },
{ {
name: "security", name: "security",
href: "/settings/security", href: "/settings/security",
icon: FiKey, icon: Key,
}, },
{ {
name: "developer", name: "developer",
href: "/settings/developer", href: "/settings/developer",
icon: FiTerminal, icon: Terminal,
}, },
{ {
name: "billing", name: "billing",
href: "/settings/billing", href: "/settings/billing",
icon: FiCreditCard, icon: CreditCard,
}, },
{ {
name: "admin", name: "admin",
href: "/settings/admin", href: "/settings/admin",
icon: FiLock, icon: Lock,
adminRequired: true, adminRequired: true,
}, },
]; ];
@@ -1,11 +1,15 @@
import type { OptionProps } from "react-select";
import { InstallAppButton } from "@calcom/app-store/components"; import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App"; import {
import { Button, Select } from "@calcom/ui"; Button,
import { FiPlus } from "@calcom/ui/components/icon"; Dropdown,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
DropdownItem,
} from "@calcom/ui";
import { Plus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
@@ -13,31 +17,6 @@ interface AdditionalCalendarSelectorProps {
isLoading?: boolean; isLoading?: boolean;
} }
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
const { t } = useLocale();
const { data } = optionProps;
return data.slug !== "add-new" ? (
<InstallAppButton
type={data.type}
render={(installProps) => {
return (
<Button {...installProps} className="flex w-full align-top" color="minimal">
{data.image && (
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
)}
<p className="text-left">{`${t("add")} ${data.label}`}</p>
</Button>
);
}}
/>
) : (
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
<FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
<p>{t("install_new_calendar_app")}</p>
</Button>
);
};
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => { const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale(); const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true }); const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
@@ -59,51 +38,39 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
type: "new_other", type: "new_other",
}); });
return ( return (
<Select <Dropdown modal={false}>
name="additionalCalendar" <DropdownMenuTrigger asChild>
placeholder={ <Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
<Button StartIcon={FiPlus} color="secondary">
{t("add")} {t("add")}
</Button> </Button>
} </DropdownMenuTrigger>
options={options} <DropdownMenuContent>
isSearchable={false} {options.map((data) => (
isLoading={isLoading} <DropdownMenuItem key={data.slug} className="focus:outline-none">
components={{ Option: ImageOption }} {data.slug === "add-new" ? (
styles={{ <DropdownItem StartIcon={Plus} color="minimal" href="/apps/categories/calendar">
menu: (defaultStyles) => ({ {t("install_new_calendar_app")}
...defaultStyles, </DropdownItem>
"@media only screen and (max-width: 640px)": { ) : (
left: "0", <InstallAppButton
}, type={data.type}
width: "max-content", render={(installProps) => {
right: "0", const props = { ...installProps } as any;
}), return (
indicatorSeparator: (defaultStyles) => ({ <DropdownItem {...props} color="minimal" type="button">
...defaultStyles, <span className="flex items-center gap-x-2">
display: "none", {data.image && <img className="h-5 w-5" src={data.image} alt={data.label} />}
}), {`${t("add")} ${data.label}`}
control: (defaultStyles) => ({ </span>
...defaultStyles, </DropdownItem>
padding: "0", );
border: "0", }}
borderRadius: "6px", />
minHeight: "auto", )}
}), </DropdownMenuItem>
dropdownIndicator: (defaultStyles) => ({ ))}
...defaultStyles, </DropdownMenuContent>
display: "none", </Dropdown>
}),
valueContainer: (defaultStyles) => ({
...defaultStyles,
padding: "0",
}),
placeholder: (defaultStyles) => ({
...defaultStyles,
margin: "0",
}),
}}
/>
); );
}} }}
/> />
+31 -34
View File
@@ -14,16 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import type { App as AppType } from "@calcom/types/App"; import type { App as AppType } from "@calcom/types/App";
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui"; import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
import { import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon";
FiBookOpen,
FiCheck,
FiExternalLink,
FiFile,
FiFlag,
FiMail,
FiPlus,
FiShield,
} from "@calcom/ui/components/icon";
/* These app slugs all require Google Cal to be installed */ /* These app slugs all require Google Cal to be installed */
@@ -96,7 +87,7 @@ const Component = ({
return ( 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"> <div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
{hasDescriptionItems && ( {hasDescriptionItems && (
<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"> <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 ? (
descriptionItems.map((descriptionItem, index) => descriptionItems.map((descriptionItem, index) =>
typeof descriptionItem === "object" ? ( typeof descriptionItem === "object" ? (
@@ -127,13 +118,17 @@ const Component = ({
<div className="mb-8 flex pt-4"> <div className="mb-8 flex pt-4">
<header> <header>
<div className="mb-4 flex items-center"> <div className="mb-4 flex items-center">
<img className="min-h-16 min-w-16 h-16 w-16" src={logo} alt={name} /> <img
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1> 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> </div>
<h2 className="text-sm font-medium text-gray-600"> <h2 className="text-default text-sm font-medium">
<Link <Link
href={`categories/${categories[0]}`} 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]} {categories[0]}
</Link>{" "} </Link>{" "}
• {t("published_by", { author })} • {t("published_by", { author })}
@@ -149,7 +144,7 @@ const Component = ({
isGlobal || isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? ( (existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3"> <div className="flex space-x-3">
<Button StartIcon={FiCheck} color="secondary" disabled> <Button StartIcon={Check} color="secondary" disabled>
{existingCredentials.length > 0 {existingCredentials.length > 0
? t("active_install", { count: existingCredentials.length }) ? t("active_install", { count: existingCredentials.length })
: t("default")} : t("default")}
@@ -171,7 +166,7 @@ const Component = ({
} }
return ( return (
<Button <Button
StartIcon={FiPlus} StartIcon={Plus}
{...props} {...props}
// @TODO: Overriding color and size prevent us from // @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now. // having to duplicate InstallAppButton for now.
@@ -243,9 +238,11 @@ const Component = ({
</span> </span>
)} )}
<div className="prose-sm prose mt-8">{body}</div> <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">
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4> {body}
<span> </div>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
<span className="text-default">
{price === 0 ? ( {price === 0 ? (
t("free_to_use_apps") t("free_to_use_apps")
) : ( ) : (
@@ -260,16 +257,16 @@ const Component = ({
)} )}
</span> </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"> <ul className="prose-sm -ml-1 -mr-1 leading-5">
{docs && ( {docs && (
<li> <li>
<a <a
target="_blank" target="_blank"
rel="noreferrer" 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}> 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")} {t("documentation")}
</a> </a>
</li> </li>
@@ -279,9 +276,9 @@ const Component = ({
<a <a
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="font-normal text-black no-underline hover:underline" className="text-emphasis font-normal no-underline hover:underline"
href={website}> 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://", "")} {website.replace("https://", "")}
</a> </a>
</li> </li>
@@ -291,9 +288,9 @@ const Component = ({
<a <a
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="font-normal text-black no-underline hover:underline" className="text-emphasis font-normal no-underline hover:underline"
href={"mailto:" + email}> 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} {email}
</a> </a>
@@ -304,9 +301,9 @@ const Component = ({
<a <a
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="font-normal text-black no-underline hover:underline" className="text-emphasis font-normal no-underline hover:underline"
href={tos}> 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")} {t("terms_of_service")}
</a> </a>
</li> </li>
@@ -316,20 +313,20 @@ const Component = ({
<a <a
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="font-normal text-black no-underline hover:underline" className="text-emphasis font-normal no-underline hover:underline"
href={privacy}> 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")} {t("privacy_policy")}
</a> </a>
</li> </li>
)} )}
</ul> </ul>
<hr className="my-8" /> <hr className="border-subtle my-8 border" />
<span className="leading-1 block text-xs text-gray-500"> <span className="leading-1 text-subtle block text-xs">
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })} {t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span> </span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}> <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> </a>
</div> </div>
</div> </div>
@@ -17,7 +17,7 @@ import {
Switch, Switch,
ShellSubHeading, ShellSubHeading,
} from "@calcom/ui"; } 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"; import { QueryCell } from "@lib/QueryCell";
@@ -99,8 +99,8 @@ function CalendarSwitch(props: {
}} }}
/> />
{!!props.destination && ( {!!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"> <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">
<FiArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
{t("adding_events_to")} {t("adding_events_to")}
</span> </span>
)} )}
@@ -177,14 +177,14 @@ function ConnectedCalendarsList(props: Props) {
credentialId={item.credentialId} credentialId={item.credentialId}
trashIcon trashIcon
onSuccess={props.onChanged} onSuccess={props.onChanged}
buttonProps={{ className: "border border-gray-300" }} buttonProps={{ className: "border border-default" }}
/> />
</div> </div>
}> }>
<div className="border-t border-gray-200"> <div className="border-subtle border-t">
{!fromOnboarding && ( {!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"> <ul className="space-y-2 px-5 py-4">
{item.calendars.map((cal) => ( {item.calendars.map((cal) => (
<CalendarSwitch <CalendarSwitch
@@ -218,7 +218,7 @@ function ConnectedCalendarsList(props: Props) {
credentialId={item.credentialId} credentialId={item.credentialId}
trashIcon trashIcon
onSuccess={props.onChanged} onSuccess={props.onChanged}
buttonProps={{ className: "border border-gray-300" }} buttonProps={{ className: "border border-default" }}
/> />
</div> </div>
} }
@@ -264,7 +264,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? ( {!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
<> <>
{heading && ( {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 <ShellSubHeading
title={t("calendar")} title={t("calendar")}
subtitle={t("installed_app_calendar_description")} subtitle={t("installed_app_calendar_description")}
@@ -279,18 +279,18 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
</div> </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="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"> <div className="bg-default border-subtle relative rounded-md border p-1.5">
<FiCalendar className="h-8 w-8" strokeWidth="1" /> <Calendar className="text-default h-8 w-8" strokeWidth="1" />
<FiPlus <Plus
className="absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2 text-black" className="text-emphasis absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2"
strokeWidth="4" strokeWidth="4"
/> />
</div> </div>
<div className="md:w-6/12"> <div className="md:w-6/12">
<h1 className="text-sm font-semibold">{t("create_events_on")}</h1> <h1 className="text-emphasis text-sm font-semibold">{t("create_events_on")}</h1>
<p className="text-sm font-normal">{t("set_calendar")}</p> <p className="text-default text-sm font-normal">{t("set_calendar")}</p>
</div> </div>
<div className="justify-end md:w-6/12"> <div className="justify-end md:w-6/12">
<DestinationCalendarSelector <DestinationCalendarSelector
@@ -322,7 +322,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
</> </>
) : ( ) : (
<EmptyScreen <EmptyScreen
Icon={FiCalendar} Icon={Calendar}
headline={t("no_category_apps", { headline={t("no_category_apps", {
category: t("calendar").toLowerCase(), category: t("calendar").toLowerCase(),
})} })}
@@ -1,139 +0,0 @@
import classNames from "classnames";
import React, { useEffect, useState } from "react";
import Select from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { DestinationCalendar } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
interface Props {
onChange: (value: { externalId: string; integration: string }) => void;
isLoading?: boolean;
hidePlaceholder?: boolean;
/** The external Id of the connected calendar */
value: string | undefined;
destinationCalendar?: DestinationCalendar | null;
}
const DestinationCalendarSelector = ({
onChange,
isLoading,
value,
hidePlaceholder,
destinationCalendar,
}: Props): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.connectedCalendars.useQuery();
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
// Extra styles to show prefixed text in react-select
const content = (hidePlaceholder = false) => {
if (!hidePlaceholder) {
return {
alignItems: "center",
display: "flex",
":before": {
content: `'${t("select_destination_calendar")}:'`,
display: "block",
marginRight: 8,
},
};
}
return {};
};
useEffect(() => {
const selected = query.data?.connectedCalendars
.map((connected) => connected.calendars ?? [])
.flat()
.find((cal) => cal.externalId === value);
if (selected) {
setSelectedOption({
value: `${selected.integration}:${selected.externalId}`,
label: `${selected.name} (${selected.externalId})` || "",
});
}
}, [query.data?.connectedCalendars, value]);
if (!query.data?.connectedCalendars.length) {
return null;
}
const options =
query.data.connectedCalendars.map((selectedCalendar) => ({
key: selectedCalendar.credentialId,
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
options: (selectedCalendar.calendars ?? [])
.filter((cal) => cal.readOnly === false)
.map((cal) => ({
label: `${cal.name} (${cal.externalId})` || "",
value: `${cal.integration}:${cal.externalId}`,
})),
})) ?? [];
const defaultCalendarSelectedString = destinationCalendar?.externalId
? `(${
destinationCalendar.externalId.length > 15
? destinationCalendar.externalId.substring(0, 15) + "..."
: destinationCalendar.externalId
})`
: "";
return (
<div
className="relative flex items-center"
title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
<Select
name="primarySelectedCalendar"
placeholder={
!hidePlaceholder ? (
`${t("select_destination_calendar")}`
) : (
<span className="whitespace-nowrap">
{t("default_calendar_selected")} {defaultCalendarSelectedString}
</span>
)
}
options={options}
styles={{
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "6px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
width: "100%",
},
};
},
}}
isSearchable={false}
className={classNames(
"block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
!hidePlaceholder && "font-medium"
)}
onChange={(option) => {
setSelectedOption(option);
if (!option) {
return;
}
/* Split only the first `:`, since Apple uses the full URL as externalId */
const [integration, externalId] = option.value.split(/:(.+)/);
onChange({
integration,
externalId,
});
}}
isLoading={isLoading}
value={selectedOption}
components={{
IndicatorSeparator: () => null,
}}
/>
</div>
);
};
export default DestinationCalendarSelector;
@@ -6,7 +6,7 @@ import React from "react";
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen } from "@calcom/ui"; import { EmptyScreen } from "@calcom/ui";
import { FiAlertCircle } from "@calcom/ui/components/icon"; import { AlertCircle } from "@calcom/ui/components/icon";
type AppsLayoutProps = { type AppsLayoutProps = {
children: React.ReactNode; children: React.ReactNode;
@@ -27,7 +27,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
<main className="w-full"> <main className="w-full">
{emptyStore ? ( {emptyStore ? (
<EmptyScreen <EmptyScreen
Icon={FiAlertCircle} Icon={AlertCircle}
headline={t("no_apps")} headline={t("no_apps")}
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""} description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
buttonText={session.data?.user.role === "ADMIN" ? t("apps_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 Shell from "@calcom/features/shell/Shell";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui"; 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)[] = [ const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{ {
name: "calendar", name: "calendar",
href: "/apps/installed/calendar", href: "/apps/installed/calendar",
icon: FiCalendar, icon: Calendar,
}, },
{ {
name: "conferencing", name: "conferencing",
href: "/apps/installed/conferencing", href: "/apps/installed/conferencing",
icon: FiVideo, icon: Video,
}, },
{ {
name: "payment", name: "payment",
href: "/apps/installed/payment", href: "/apps/installed/payment",
icon: FiCreditCard, icon: CreditCard,
}, },
{ {
name: "automation", name: "automation",
href: "/apps/installed/automation", href: "/apps/installed/automation",
icon: FiShare2, icon: Share2,
}, },
{ {
name: "analytics", name: "analytics",
href: "/apps/installed/analytics", href: "/apps/installed/analytics",
icon: FiBarChart, icon: BarChart,
}, },
{ {
name: "other", name: "other",
href: "/apps/installed/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"}> <div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
<Label className="mt-4"> {t("2fa_code")}</Label> <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")} /> <input hidden type="hidden" value={value} {...methods.register("totpCode")} />
<div className="flex flex-row justify-between"> <div className="flex flex-row justify-between">
<Input <Input
@@ -26,7 +26,7 @@ export default function AdminLayout({
const isAppsPage = router.asPath.startsWith("/settings/admin/apps"); const isAppsPage = router.asPath.startsWith("/settings/admin/apps");
return ( return (
<SettingsLayout {...rest}> <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"}> <div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
<ErrorBoundary>{children}</ErrorBoundary> <ErrorBoundary>{children}</ErrorBoundary>
</div> </div>
@@ -1,13 +1,13 @@
import React from "react"; import React from "react";
import { Button, SkeletonText } from "@calcom/ui"; 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"; import classNames from "@lib/classNames";
function SkeletonLoader() { function SkeletonLoader() {
return ( 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 /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
@@ -20,8 +20,8 @@ export default SkeletonLoader;
function SkeletonItem() { function SkeletonItem() {
return ( return (
<li> <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="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 hover:bg-neutral-50 sm:px-6"> <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-4 w-32" />
<SkeletonText className="my-1 h-2 w-24" /> <SkeletonText className="my-1 h-2 w-24" />
<SkeletonText className="my-1 h-2 w-40" /> <SkeletonText className="my-1 h-2 w-40" />
@@ -31,7 +31,7 @@ function SkeletonItem() {
type="button" type="button"
variant="icon" variant="icon"
color="secondary" color="secondary"
StartIcon={FiMoreHorizontal} StartIcon={MoreHorizontal}
disabled disabled
/> />
</div> </div>
@@ -43,7 +43,7 @@ export const SelectSkeletonLoader = ({ className }: { className?: string }) => {
return ( return (
<li <li
className={classNames( 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 className
)}> )}>
<div className="flex-grow truncate text-sm"> <div className="flex-grow truncate text-sm">
@@ -4,7 +4,7 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "@calcom/ui"; 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"; import type { Props } from "./pages/AvailabilityPage";
@@ -41,7 +41,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
return ( return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium"> <div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? ( {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 " /> <Link className="text-default min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] " />
) : ( ) : (
<img <img
src={eventLocationType.iconUrl} src={eventLocationType.iconUrl}
@@ -61,7 +61,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[]; const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[];
return filteredLocations.length ? ( return filteredLocations.length ? (
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600"> <div className="text-default mr-6 flex w-full flex-col space-y-4 break-words text-sm">
{filteredLocations} {filteredLocations}
</div> </div>
) : null; ) : null;
+19 -7
View File
@@ -10,6 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { TimeFormat } from "@calcom/lib/timeFormat"; import { TimeFormat } from "@calcom/lib/timeFormat";
import { nameOfDay } from "@calcom/lib/weekday"; import { nameOfDay } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots"; import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui"; import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
@@ -28,6 +29,7 @@ type AvailableTimesProps = {
slots?: Slot[]; slots?: Slot[];
isLoading: boolean; isLoading: boolean;
ethSignature?: string; ethSignature?: string;
duration: number;
}; };
const AvailableTimes: FC<AvailableTimesProps> = ({ const AvailableTimes: FC<AvailableTimesProps> = ({
@@ -42,7 +44,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
seatsPerTimeSlot, seatsPerTimeSlot,
bookingAttendees, bookingAttendees,
ethSignature, ethSignature,
duration,
}) => { }) => {
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>(); const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
const router = useRouter(); const router = useRouter();
@@ -63,16 +67,24 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
[isMobile] [isMobile]
); );
const reserveSlot = (slot: Slot) => {
reserveSlotMutation.mutate({
slotUtcStartDate: slot.time,
eventTypeId,
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
});
};
return ( return (
<div ref={slotPickerRef}> <div ref={slotPickerRef}>
{!!date ? ( {!!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="mb-4 flex items-center text-left text-base">
<div className="mr-4"> <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")} {nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span> </span>
<span className="text-bookinglight"> <span className="text-subtle">
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")} , {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
</span> </span>
</div> </div>
@@ -132,7 +144,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
{seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? ( {seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? (
<div <div
className={classNames( 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" ? "" : "" brand === "#fff" || brand === "#ffffff" ? "" : ""
)}> )}>
{dayjs(slot.time).tz(timeZone()).format(timeFormat)} {dayjs(slot.time).tz(timeZone()).format(timeFormat)}
@@ -147,10 +159,10 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
href={bookingUrl} href={bookingUrl}
prefetch={false} prefetch={false}
className={classNames( className={classNames(
"hover:bg-brand hover:border-brand hover:text-brandcontrast dark:hover:text-darkmodebrandcontrast", " 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",
"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",
brand === "#fff" || brand === "#ffffff" ? "" : "" brand === "#fff" || brand === "#ffffff" ? "" : ""
)} )}
onClick={() => reserveSlot(slot)}
data-testid="time"> data-testid="time">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)} {dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && ( {!!seatsPerTimeSlot && (
@@ -174,7 +186,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
{!isLoading && !slots.length && ( {!isLoading && !slots.length && (
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center"> <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> </div>
)} )}
@@ -6,7 +6,7 @@ import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge } from "@calcom/ui"; 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"; import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -83,32 +83,27 @@ const BookingDescription: FC<Props> = (props) => {
size="sm" size="sm"
truncateAfter={3} truncateAfter={3}
/> />
<h2 className="mt-1 mb-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300"> <h2 className="text-default mt-1 mb-2 break-words text-sm font-medium ">{profile.name}</h2>
{profile.name} <h1 className="font-cal text-emphasis mb-6 break-words text-2xl font-semibold leading-none">
</h2>
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold leading-none text-gray-900">
{eventType.title} {eventType.title}
</h1> </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 && ( {eventType?.description && (
<div <div
className={classNames( className={classNames(
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ", "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"> <div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
<EventTypeDescriptionSafeHTML eventType={eventType} /> <EventTypeDescriptionSafeHTML eventType={eventType} />
</div> </div>
</div> </div>
)} )}
{requiresConfirmation && ( {requiresConfirmation && (
<div <div className={classNames("items-top flex", isBookingPage && "text-default text-sm font-medium")}>
className={classNames(
"items-top flex",
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
)}>
<div> <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> </div>
{requiresConfirmationText} {requiresConfirmationText}
</div> </div>
@@ -119,12 +114,12 @@ const BookingDescription: FC<Props> = (props) => {
<div <div
className={classNames( className={classNames(
"flex flex-nowrap text-sm font-medium", "flex flex-nowrap text-sm font-medium",
isBookingPage && "dark:text-darkgray-600 text-gray-600", isBookingPage && "text-default",
!eventType.metadata?.multipleDuration && "items-center" !eventType.metadata?.multipleDuration && "items-center"
)}> )}>
<FiClock <Clock
className={classNames( 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]" isBookingPage && "mt-[2px]"
)} )}
/> />
@@ -137,9 +132,9 @@ const BookingDescription: FC<Props> = (props) => {
variant="gray" variant="gray"
className={classNames( className={classNames(
duration === dur.toString() duration === dur.toString()
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast" ? "bg-emphasis border-emphasis text-emphasis "
: "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", : "bg-subtle text-default border-transparent ",
"cursor-pointer" "cursor-pointer border"
)} )}
onClick={() => { onClick={() => {
setDuration(dur); setDuration(dur);
@@ -1,22 +1,35 @@
import type { TFunction } from "next-i18next";
import { FormattedNumber, IntlProvider } from "react-intl"; import { FormattedNumber, IntlProvider } from "react-intl";
import getPaymentAppData from "@calcom/lib/getPaymentAppData"; 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); const paymentAppData = getPaymentAppData(props.eventType);
if (!paymentAppData || paymentAppData.price <= 0) return null; if (!paymentAppData || paymentAppData.price <= 0) return null;
return ( return (
<p className="text-bookinglight -ml-2 px-2 text-sm "> <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]" /> <CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en"> {paymentAppData.paymentOption === "HOLD" ? (
<FormattedNumber <>
value={paymentAppData.price / 100.0} {props.t("no_show_fee_amount", {
style="currency" amount: paymentAppData.price / 100.0,
currency={paymentAppData.currency?.toUpperCase()} formatParams: { amount: { currency: paymentAppData.currency } },
/> })}
</IntlProvider> </>
) : (
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
)}
</p> </p>
); );
}; };
+73 -36
View File
@@ -28,10 +28,11 @@ import {
TableActions, TableActions,
TextAreaField, TextAreaField,
} from "@calcom/ui"; } 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 useMeQuery from "@lib/hooks/useMeQuery";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog"; import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
@@ -56,7 +57,9 @@ function BookingListItem(booking: BookingItemProps) {
const router = useRouter(); const router = useRouter();
const [rejectionReason, setRejectionReason] = useState<string>(""); const [rejectionReason, setRejectionReason] = useState<string>("");
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false); const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false); const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
const cardCharged = booking?.payment[0]?.success;
const mutation = trpc.viewer.bookings.confirm.useMutation({ const mutation = trpc.viewer.bookings.confirm.useMutation({
onSuccess: (data) => { onSuccess: (data) => {
if (data?.status === BookingStatus.REJECTED) { if (data?.status === BookingStatus.REJECTED) {
@@ -113,19 +116,23 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => { onClick: () => {
setRejectionDialogIsOpen(true); setRejectionDialogIsOpen(true);
}, },
icon: FiSlash, icon: Slash,
disabled: mutation.isLoading, disabled: mutation.isLoading,
}, },
{ // For bookings with payment, only confirm if the booking is paid for
id: "confirm", ...((isPending && !booking?.eventType?.price) || (!!booking?.eventType?.price && booking.paid)
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"), ? [
onClick: () => { {
bookingConfirm(true); id: "confirm",
}, label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
icon: FiCheck, onClick: () => {
disabled: mutation.isLoading, bookingConfirm(true);
color: "primary", },
}, icon: Check,
disabled: mutation.isLoading,
},
]
: []),
]; ];
const showRecordingActions: ActionType[] = [ const showRecordingActions: ActionType[] = [
@@ -149,7 +156,7 @@ function BookingListItem(booking: BookingItemProps) {
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : "" isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""} }${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
`, `,
icon: FiX, icon: X,
}, },
{ {
id: "edit_booking", id: "edit_booking",
@@ -157,7 +164,7 @@ function BookingListItem(booking: BookingItemProps) {
actions: [ actions: [
{ {
id: "reschedule", id: "reschedule",
icon: FiClock, icon: Clock,
label: t("reschedule_booking"), label: t("reschedule_booking"),
href: `/reschedule/${booking.uid}${ href: `/reschedule/${booking.uid}${
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : "" booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
@@ -165,7 +172,7 @@ function BookingListItem(booking: BookingItemProps) {
}, },
{ {
id: "reschedule_request", id: "reschedule_request",
icon: FiSend, icon: Send,
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ", iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
label: t("send_reschedule_request"), label: t("send_reschedule_request"),
onClick: () => { onClick: () => {
@@ -178,12 +185,24 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => { onClick: () => {
setIsOpenLocationDialog(true); 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) { if (isTabRecurring && isRecurring) {
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking"); bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
} }
@@ -194,7 +213,7 @@ function BookingListItem(booking: BookingItemProps) {
const RequestSentMessage = () => { const RequestSentMessage = () => {
return ( return (
<Badge startIcon={FiSend} size="md" variant="gray" data-testid="request_reschedule_sent"> <Badge startIcon={Send} size="md" variant="gray" data-testid="request_reschedule_sent">
{t("reschedule_request_sent")} {t("reschedule_request_sent")}
</Badge> </Badge>
); );
@@ -237,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 = const showRecordingsButtons =
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed; (booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
const title = booking.title;
return ( return (
<> <>
<RescheduleDialog <RescheduleDialog
@@ -254,6 +277,15 @@ function BookingListItem(booking: BookingItemProps) {
isOpenDialog={isOpenSetLocationDialog} isOpenDialog={isOpenSetLocationDialog}
setShowLocationModal={setIsOpenLocationDialog} setShowLocationModal={setIsOpenLocationDialog}
/> />
{booking.paid && (
<ChargeCardDialog
isOpenDialog={chargeCardDialogIsOpen}
setIsOpenDialog={setChargeCardDialogIsOpen}
bookingId={booking.id}
paymentAmount={booking?.payment[0].amount}
paymentCurrency={booking?.payment[0].currency}
/>
)}
{showRecordingsButtons && ( {showRecordingsButtons && (
<ViewRecordingsDialog <ViewRecordingsDialog
booking={booking} booking={booking}
@@ -271,7 +303,7 @@ function BookingListItem(booking: BookingItemProps) {
label={ label={
<> <>
{t("rejection_reason")} {t("rejection_reason")}
<span className="font-normal text-gray-500"> (Optional)</span> <span className="text-subtle font-normal"> (Optional)</span>
</> </>
} }
value={rejectionReason} value={rejectionReason}
@@ -292,13 +324,13 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent> </DialogContent>
</Dialog> </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 <td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]" className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}> onClick={onClickTableData}>
<div className="cursor-pointer py-4"> <div className="cursor-pointer py-4">
<div className="text-sm leading-6 text-gray-900">{startTime}</div> <div className="text-emphasis text-sm leading-6">{startTime}</div>
<div className="text-sm text-gray-500"> <div className="text-subtle text-sm">
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "} {formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)} {formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
<MeetingTimeInTimezones <MeetingTimeInTimezones
@@ -321,11 +353,11 @@ function BookingListItem(booking: BookingItemProps) {
)} )}
{booking.paid && ( {booking.paid && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="green"> <Badge className="ltr:mr-2 rtl:ml-2" variant="green">
{t("paid")} {booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
</Badge> </Badge>
)} )}
{recurringDates !== undefined && ( {recurringDates !== undefined && (
<div className="mt-2 text-sm text-gray-400"> <div className="text-muted mt-2 text-sm">
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} /> <RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
</div> </div>
)} )}
@@ -335,8 +367,8 @@ function BookingListItem(booking: BookingItemProps) {
{/* Time and Badges for mobile */} {/* Time and Badges for mobile */}
<div className="w-full pt-4 pb-2 sm:hidden"> <div className="w-full pt-4 pb-2 sm:hidden">
<div className="flex w-full items-center justify-between 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="text-emphasis text-sm leading-6">{startTime}</div>
<div className="pr-2 text-sm text-gray-500"> <div className="text-subtle pr-2 text-sm">
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "} {formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)} {formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
<MeetingTimeInTimezones <MeetingTimeInTimezones
@@ -365,7 +397,7 @@ function BookingListItem(booking: BookingItemProps) {
</Badge> </Badge>
)} )}
{recurringDates !== undefined && ( {recurringDates !== undefined && (
<div className="text-sm text-gray-400 sm:hidden"> <div className="text-muted text-sm sm:hidden">
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} /> <RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
</div> </div>
)} )}
@@ -375,21 +407,21 @@ function BookingListItem(booking: BookingItemProps) {
<div <div
title={title} title={title}
className={classNames( 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" : "" isCancelled ? "line-through" : ""
)}> )}>
{title} {title}
<span> </span> <span> </span>
{!!booking?.eventType?.price && !booking.paid && ( {!!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")} {t("pending_payment")}
</Badge> </Badge>
)} )}
</div> </div>
{booking.description && ( {booking.description && (
<div <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}> title={booking.description}>
&quot;{booking.description}&quot; &quot;{booking.description}&quot;
</div> </div>
@@ -413,7 +445,7 @@ function BookingListItem(booking: BookingItemProps) {
<> <>
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />} {isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
{isConfirmed && <TableActions actions={bookedActions} />} {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} ) : null}
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null} {isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
@@ -423,6 +455,11 @@ function BookingListItem(booking: BookingItemProps) {
<RequestSentMessage /> <RequestSentMessage />
</div> </div>
)} )}
{booking.status === "ACCEPTED" && booking.paid && booking?.payment[0]?.paymentOption === "HOLD" && (
<div className="ml-2">
<TableActions actions={chargeCardActions} />
</div>
)}
</td> </td>
</tr> </tr>
</> </>
@@ -475,10 +512,10 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
</p> </p>
); );
})}> })}>
<div className="text-gray-600 dark:text-white"> <div className="text-default">
<FiRefreshCcw <RefreshCcw
strokeWidth="3" 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"> <p className="mt-1 pl-5 text-xs">
{booking.status === BookingStatus.ACCEPTED {booking.status === BookingStatus.ACCEPTED
@@ -551,13 +588,13 @@ const DisplayAttendees = ({
}) => { }) => {
const { t } = useLocale(); const { t } = useLocale();
return ( return (
<div className="text-sm text-gray-900"> <div className="text-emphasis text-sm">
{user && <FirstAttendee user={user} currentEmail={currentEmail} />} {user && <FirstAttendee user={user} currentEmail={currentEmail} />}
{attendees.length > 1 ? <span>,&nbsp;</span> : <span>&nbsp;{t("and")}&nbsp;</span>} {attendees.length > 1 ? <span>,&nbsp;</span> : <span>&nbsp;{t("and")}&nbsp;</span>}
<Attendee {...attendees[0]} /> <Attendee {...attendees[0]} />
{attendees.length > 1 && ( {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 ? ( {attendees.length > 2 ? (
<Tooltip <Tooltip
content={attendees.slice(1).map((attendee) => ( content={attendees.slice(1).map((attendee) => (
@@ -2,11 +2,10 @@ import { useRouter } from "next/router";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RecurringEvent } from "@calcom/types/Calendar"; import type { RecurringEvent } from "@calcom/types/Calendar";
import { Button, TextArea } from "@calcom/ui"; import { Button, TextArea } from "@calcom/ui";
import { FiX } from "@calcom/ui/components/icon"; import { X } from "@calcom/ui/components/icon";
type Props = { type Props = {
booking: { booking: {
@@ -34,7 +33,6 @@ export default function CancelBooking(props: Props) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled")); const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
useTheme(props.theme);
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => { const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
if (node !== null) { if (node !== null) {
@@ -47,11 +45,11 @@ export default function CancelBooking(props: Props) {
<> <>
{error && ( {error && (
<div className="mt-8"> <div className="mt-8">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100"> <div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<FiX className="h-6 w-6 text-red-600" /> <X className="h-6 w-6 text-red-600" />
</div> </div>
<div className="mt-3 text-center sm:mt-5"> <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} {error}
</h3> </h3>
</div> </div>
@@ -59,13 +57,13 @@ export default function CancelBooking(props: Props) {
)} )}
{!error && ( {!error && (
<div className="mt-5 sm:mt-6"> <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 <TextArea
ref={cancelBookingRef} ref={cancelBookingRef}
placeholder={t("cancellation_reason_placeholder")} placeholder={t("cancellation_reason_placeholder")}
value={cancellationReason} value={cancellationReason}
onChange={(e) => setCancellationReason(e.target.value)} 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} rows={3}
/> />
<div className="flex flex-col-reverse rtl:space-x-reverse "> <div className="flex flex-col-reverse rtl:space-x-reverse ">
@@ -77,7 +75,6 @@ export default function CancelBooking(props: Props) {
{t("nevermind")} {t("nevermind")}
</Button> </Button>
<Button <Button
className="dark:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkmodebrandcontrast bg-brand hover:border-brand text-brandcontrast flex justify-center"
data-testid="cancel" data-testid="cancel"
onClick={async () => { onClick={async () => {
setLoading(true); setLoading(true);
@@ -4,7 +4,7 @@ import { SkeletonText } from "@calcom/ui";
function SkeletonLoader() { function SkeletonLoader() {
return ( 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 /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
+2 -1
View File
@@ -164,7 +164,7 @@ export const SlotPicker = ({
isLoading={isLoading} isLoading={isLoading}
className={classNames( className={classNames(
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]", "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)} includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
locale={isLocaleReady ? i18n.language : "en"} locale={isLocaleReady ? i18n.language : "en"}
@@ -194,6 +194,7 @@ export const SlotPicker = ({
bookingAttendees={bookingAttendees} bookingAttendees={bookingAttendees}
recurringCount={recurringEventCount} recurringCount={recurringEventCount}
ethSignature={ethSignature} ethSignature={ethSignature}
duration={parseInt(duration)}
/> />
</> </>
); );
+2 -1
View File
@@ -27,7 +27,8 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
<TimezoneSelect <TimezoneSelect
id="timeZone" id="timeZone"
classNames={{ classNames={{
singleValue: () => "dark:text-darkgray-600 text-gray-600", singleValue: () => "text-default",
dropdownIndicator: () => "text-default",
menu: () => "!w-64 max-w-[90vw]", menu: () => "!w-64 max-w-[90vw]",
}} }}
variant="minimal" variant="minimal"
@@ -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"; import { timeZone as localStorageTimeZone } from "@lib/clock";
@@ -18,7 +18,7 @@ export function TimezoneDropdown({
return ( return (
<> <>
<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"> <div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" /> <Globe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
<TimeOptions onSelectTimeZone={handleSelectTimeZone} /> <TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</div> </div>
</> </>
@@ -14,18 +14,17 @@ import {
useIsBackgroundTransparent, useIsBackgroundTransparent,
useIsEmbed, useIsEmbed,
} from "@calcom/embed-core/embed-iframe"; } from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import getPaymentAppData from "@calcom/lib/getPaymentAppData"; import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
import notEmpty from "@calcom/lib/notEmpty"; import notEmpty from "@calcom/lib/notEmpty";
import { getRecurringFreq } from "@calcom/lib/recurringStrings"; import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat"; import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc"; import { trpc } from "@calcom/trpc";
import { HeadSeo } from "@calcom/ui"; import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
import { FiCreditCard, FiUser, FiRefreshCcw } from "@calcom/ui/components/icon"; import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock"; import { timeZone as localStorageTimeZone } from "@lib/clock";
@@ -58,12 +57,24 @@ const dateQuerySchema = z.object({
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps; 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 AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
const router = useRouter(); const router = useRouter();
const isEmbed = useIsEmbed(restProps.isEmbed); const isEmbed = useIsEmbed(restProps.isEmbed);
const query = dateQuerySchema.parse(router.query); const query = dateQuerySchema.parse(router.query);
const { rescheduleUid } = query; const { rescheduleUid } = query;
useTheme(profile.theme); useTheme(profile.theme);
useBrandColors({
brandColor: profile.brandColor,
darkBrandColor: profile.darkBrandColor,
});
const { t } = useLocale(); const { t } = useLocale();
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker"); const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
//TODO: Plan to remove shouldAlignCentrallyInEmbed config //TODO: Plan to remove shouldAlignCentrallyInEmbed config
@@ -93,8 +104,9 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count); const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
/*
const telemetry = useTelemetry(); const telemetry = useTelemetry();
useEffect(() => { useEffect(() => {
if (top !== window) { if (top !== window) {
//page_view will be collected automatically by _middleware.ts //page_view will be collected automatically by _middleware.ts
telemetry.event( telemetry.event(
@@ -102,7 +114,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") }) collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
); );
} }
}, [telemetry]); }, [telemetry]); */
const embedUiConfig = useEmbedUiConfig(); const embedUiConfig = useEmbedUiConfig();
// get dynamic user list here // get dynamic user list here
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : []; const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
@@ -112,6 +124,16 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
[timeZone] [timeZone]
); );
const paymentAppData = getPaymentAppData(eventType); 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 rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
const rawSlug = profile.slug ? profile.slug.split("/") : []; 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. if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
@@ -157,30 +179,27 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
isBrandingHidden={restProps.isBrandingHidden} isBrandingHidden={restProps.isBrandingHidden}
/> />
<BookingPageTagManager eventType={eventType} /> <BookingPageTagManager eventType={eventType} />
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<div> <div>
<main <main
className={classNames( className={classNames(
"flex flex-col md:mx-4", "flex flex-col md:mx-4",
shouldAlignCentrally ? "items-center" : "items-start", 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>
<div <div
style={availabilityDatePickerEmbedStyles} style={availabilityDatePickerEmbedStyles}
className={classNames( className={classNames(
isBackgroundTransparent isBackgroundTransparent ? "" : "bg-default dark:bg-muted pb-4 md:pb-0",
? "" "border-booker md:border-booker-width md:rounded-md",
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
"border-bookinglightest md:rounded-md md:border",
isEmbed && "mx-auto" isEmbed && "mx-auto"
)}> )}>
<div className="md:flex"> <div className="md:flex">
{showEventTypeDetails && ( {showEventTypeDetails && (
<div <div
className={classNames( className={classNames(
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r", " border-subtle flex flex-col p-5 sm:border-r",
"min-w-full md:w-[280px] md:min-w-[280px]", "min-w-full md:w-[230px] md:min-w-[230px]",
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]" recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
)}> )}>
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}> <BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
@@ -188,10 +207,10 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
<div <div
className={classNames( className={classNames(
"flex flex-nowrap items-center text-sm font-medium", "flex flex-nowrap items-center text-sm font-medium",
"dark:text-darkgray-600 text-gray-600", " text-default",
"ltr:mr-[10px] rtl:ml-[10px]" "ltr:mr-[10px] rtl:ml-[10px]"
)}> )}>
<FiUser <User
className={classNames( className={classNames(
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]", "min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
"mt-[2px]" "mt-[2px]"
@@ -202,21 +221,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
)} )}
{!rescheduleUid && eventType.recurringEvent && ( {!rescheduleUid && eventType.recurringEvent && (
<div className="flex items-start text-sm font-medium"> <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> <div>
<p className="mb-1 -ml-2 inline px-2 py-1"> <p className="mb-1 -ml-2 inline px-2 py-1">
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })} {getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
</p> </p>
<input
type="number" <NumberInput
defaultValue={eventType.recurringEvent.count}
min="1" min="1"
max={eventType.recurringEvent.count} 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" isFullWidth={false}
defaultValue={eventType.recurringEvent.count} className="me-2 inline w-16"
onChange={(event) => { onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value)); setRecurringEventCount(parseInt(event?.target.value));
}} }}
/> />
<p className="inline"> <p className="inline">
{t("occurrence", { {t("occurrence", {
count: recurringEventCount, count: recurringEventCount,
@@ -227,14 +248,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
)} )}
{paymentAppData.price > 0 && ( {paymentAppData.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium"> <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]" /> <CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en"> {paymentAppData.paymentOption === "HOLD" ? (
<FormattedNumber <>
value={paymentAppData.price / 100.0} {t("no_show_fee_amount", {
style="currency" amount: paymentAppData.price / 100.0,
currency={paymentAppData.currency?.toUpperCase()} formatParams: { amount: { currency: paymentAppData.currency } },
/> })}
</IntlProvider> </>
) : (
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
)}
</p> </p>
)} )}
{timezoneDropdown} {timezoneDropdown}
@@ -243,12 +273,12 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
{/* Temporarily disabled - booking?.startTime && rescheduleUid && ( {/* Temporarily disabled - booking?.startTime && rescheduleUid && (
<div> <div>
<p <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"> data-testid="former_time_p_desktop">
{t("former_time")} {t("former_time")}
</p> </p>
<p className="text-gray-500 line-through dark:text-darkgray-600"> <p className="text-subtle line-through ">
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" /> <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)} {typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
</p> </p>
</div> </div>
@@ -11,7 +11,6 @@ import { z } from "zod";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager"; import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import type { EventLocationType } from "@calcom/app-store/locations"; 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 { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import { getEventTypeAppData } from "@calcom/app-store/utils"; import { getEventTypeAppData } from "@calcom/app-store/utils";
import type { LocationObject } from "@calcom/core/location"; import type { LocationObject } from "@calcom/core/location";
@@ -29,19 +28,22 @@ import {
import getBookingResponsesSchema, { import getBookingResponsesSchema, {
getBookingResponsesPartialSchema, getBookingResponsesPartialSchema,
} from "@calcom/features/bookings/lib/getBookingResponsesSchema"; } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder"; 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 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 { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { TimeFormat } from "@calcom/lib/timeFormat"; import { TimeFormat } from "@calcom/lib/timeFormat";
import { Button, Form, Tooltip } from "@calcom/ui"; import { trpc } from "@calcom/trpc";
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon"; import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock"; import { timeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery"; import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -64,6 +66,14 @@ const BookingDescriptionPayment = dynamic(
() => import("@components/booking/BookingDescriptionPayment") () => import("@components/booking/BookingDescriptionPayment")
) as unknown as typeof import("@components/booking/BookingDescriptionPayment").default; ) 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; type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
const BookingFields = ({ const BookingFields = ({
fields, fields,
@@ -135,26 +145,14 @@ const BookingFields = ({
} }
const optionsInputs = field.optionsInputs; const optionsInputs = field.optionsInputs;
const options = locations.map((location) => { // TODO: Instead of `getLocationOptionsForSelect` options should be retrieved from dataStore[field.getOptionsAt]. It would make it agnostic of the `name` of the field.
const eventLocation = getEventLocationType(location.type); const options = getLocationOptionsForSelect(locations, t);
const locationString = locationKeyToString(location); options.forEach((option) => {
const optionInput = optionsInputs[option.value as keyof typeof optionsInputs];
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];
if (optionInput) { if (optionInput) {
optionInput.placeholder = t(eventLocation?.attendeeInputPlaceholder || ""); optionInput.placeholder = option.inputPlaceholder;
} }
return {
label: t(locationString),
value: type,
};
}); });
field.options = options.filter( field.options = options.filter(
(location): location is NonNullable<(typeof options)[number]> => !!location (location): location is NonNullable<(typeof options)[number]> => !!location
); );
@@ -213,8 +211,11 @@ const BookingPage = ({
hashedLink, hashedLink,
...restProps ...restProps
}: BookingPageProps) => { }: BookingPageProps) => {
const removeSelectedSlotMarkMutation = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation();
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
const { duration: queryDuration } = useRouterQuery("duration"); const { duration: queryDuration } = useRouterQuery("duration");
const { date: queryDate } = useRouterQuery("date");
const isEmbed = useIsEmbed(restProps.isEmbed); const isEmbed = useIsEmbed(restProps.isEmbed);
const embedUiConfig = useEmbedUiConfig(); const embedUiConfig = useEmbedUiConfig();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left"; const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
@@ -230,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 // Define duration now that we support multiple duration eventTypes
let duration = eventType.length; let duration = eventType.length;
if ( if (
@@ -242,13 +252,19 @@ const BookingPage = ({
} }
useEffect(() => { useEffect(() => {
if (top !== window) { /* if (top !== window) {
//page_view will be collected automatically by _middleware.ts //page_view will be collected automatically by _middleware.ts
telemetry.event( telemetry.event(
telemetryEventTypes.embedView, telemetryEventTypes.embedView,
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") }) 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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@@ -268,15 +284,19 @@ const BookingPage = ({
); );
} }
return router.push({ const query = {
pathname: `/booking/${uid}`, isSuccessBookingPage: true,
query: { email: bookingForm.getValues("responses.email"),
isSuccessBookingPage: true, eventTypeSlug: eventType.slug,
email: bookingForm.getValues("responses.email"), seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
eventTypeSlug: eventType.slug, ...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null, };
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
}, return bookingSuccessRedirect({
router,
successRedirectUrl: eventType.successRedirectUrl,
query,
bookingUid: uid,
}); });
}, },
}); });
@@ -284,16 +304,18 @@ const BookingPage = ({
const recurringMutation = useMutation(createRecurringBooking, { const recurringMutation = useMutation(createRecurringBooking, {
onSuccess: async (responseData = []) => { onSuccess: async (responseData = []) => {
const { uid } = responseData[0] || {}; const { uid } = responseData[0] || {};
const query = {
return router.push({ isSuccessBookingPage: true,
pathname: `/booking/${uid}`, allRemainingBookings: true,
query: { email: bookingForm.getValues("responses.email"),
isSuccessBookingPage: true, eventTypeSlug: eventType.slug,
allRemainingBookings: true, formerTime: booking?.startTime.toString(),
email: bookingForm.getValues("responses.email"), };
eventTypeSlug: eventType.slug, return bookingSuccessRedirect({
formerTime: booking?.startTime.toString(), router,
}, successRedirectUrl: eventType.successRedirectUrl,
query,
bookingUid: uid,
}); });
}, },
}); });
@@ -303,6 +325,10 @@ const BookingPage = ({
} = useTypedQuery(routerQuerySchema); } = useTypedQuery(routerQuerySchema);
useTheme(profile.theme); useTheme(profile.theme);
useBrandColors({
brandColor: profile.brandColor,
darkBrandColor: profile.darkBrandColor,
});
const querySchema = getBookingResponsesPartialSchema({ const querySchema = getBookingResponsesPartialSchema({
eventType: { eventType: {
@@ -512,7 +538,6 @@ const BookingPage = ({
<link rel="icon" href="/favico.ico" /> <link rel="icon" href="/favico.ico" />
</Head> </Head>
<BookingPageTagManager eventType={eventType} /> <BookingPageTagManager eventType={eventType} />
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<main <main
className={classNames( className={classNames(
shouldAlignCentrally ? "mx-auto" : "", shouldAlignCentrally ? "mx-auto" : "",
@@ -522,17 +547,17 @@ const BookingPage = ({
<div <div
className={classNames( className={classNames(
"main", "main",
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border", isBackgroundTransparent ? "" : "bg-default dark:bg-muted",
"dark:border-darkgray-300 rounded-md sm:border" "border-booker sm:border-booker-width rounded-md"
)}> )}>
<div className="sm:flex"> <div className="sm:flex">
{showEventTypeDetails && ( {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}> <BookingDescription isBookingPage profile={profile} eventType={eventType}>
<BookingDescriptionPayment eventType={eventType} /> <BookingDescriptionPayment eventType={eventType} t={t} />
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && ( {!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
<div className="items-start text-sm font-medium text-gray-600 dark:text-white"> <div className="dark:text-inverted text-default items-start text-sm font-medium">
<FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" /> <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"> <p className="-ml-2 inline-block items-center px-2">
{getEveryFreqFor({ {getEveryFreqFor({
t, t,
@@ -543,7 +568,7 @@ const BookingPage = ({
</div> </div>
)} )}
<div className="text-bookinghighlight flex items-start text-sm"> <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"> <div className="text-sm font-medium">
{isClientTimezoneAvailable && {isClientTimezoneAvailable &&
(rescheduleUid || !eventType.recurringEvent?.freq) && (rescheduleUid || !eventType.recurringEvent?.freq) &&
@@ -560,7 +585,7 @@ const BookingPage = ({
content={recurringStrings.slice(5).map((timeFormatted, key) => ( content={recurringStrings.slice(5).map((timeFormatted, key) => (
<p key={key}>{timeFormatted}</p> <p key={key}>{timeFormatted}</p>
))}> ))}>
<p className="dark:text-darkgray-600 text-sm"> <p className=" text-sm">
+ {t("plus_more", { count: recurringStrings.length - 5 })} + {t("plus_more", { count: recurringStrings.length - 5 })}
</p> </p>
</Tooltip> </Tooltip>
@@ -574,7 +599,7 @@ const BookingPage = ({
{t("former_time")} {t("former_time")}
</p> </p>
<p className="line-through "> <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 && {isClientTimezoneAvailable &&
typeof booking.startTime === "string" && typeof booking.startTime === "string" &&
parseDate(dayjs(booking.startTime), i18n, timeFormat)} parseDate(dayjs(booking.startTime), i18n, timeFormat)}
@@ -583,7 +608,7 @@ const BookingPage = ({
)} )}
{!!eventType.seatsPerTimeSlot && ( {!!eventType.seatsPerTimeSlot && (
<div className="text-bookinghighlight flex items-start text-sm"> <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] ${ className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
currentSlotBooking && currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5 currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
@@ -637,16 +662,15 @@ const BookingPage = ({
</Button> </Button>
<Button <Button
type="submit" type="submit"
className="dark:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkmodebrandcontrast bg-brand hover:border-brand text-brandcontrast"
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"} data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
loading={mutation.isLoading || recurringMutation.isLoading}> loading={mutation.isLoading || recurringMutation.isLoading}>
{rescheduleUid ? t("reschedule") : t("confirm")} {rescheduleUid ? t("reschedule") : t("confirm")}
</Button> </Button>
</div> </div>
</Form> </Form>
{(mutation.isError || recurringMutation.isError) && ( {mutation.isError || recurringMutation.isError ? (
<ErrorMessage error={mutation.error || recurringMutation.error} /> <ErrorMessage error={mutation.error || recurringMutation.error} />
)} ) : null}
</div> </div>
</div> </div>
</div> </div>
@@ -667,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 data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <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>
<div className="ltr:ml-3 rtl:mr-3"> <div className="ms-3">
<p className="text-sm text-blue-700"> <p className="text-sm text-blue-700">
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "} {rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
{error instanceof HttpError || error instanceof Error ? ( {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>
);
};
@@ -19,7 +19,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Input } from "@calcom/ui"; import { Input } from "@calcom/ui";
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } 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"; import { QueryCell } from "@lib/QueryCell";
@@ -164,7 +164,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
return ( return (
<div> <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 || "")} {t(eventLocationType.messageForOrganizer || "")}
</label> </label>
<div className="mt-1"> <div className="mt-1">
@@ -174,6 +174,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
id="locationInput" id="locationInput"
placeholder={t(eventLocationType.organizerInputPlaceholder || "")} placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
required required
className="border-default block w-full rounded-sm text-sm"
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
defaultValue={ defaultValue={
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
@@ -182,7 +183,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
<ErrorMessage <ErrorMessage
errors={locationFormMethods.formState.errors} errors={locationFormMethods.formState.errors}
name={eventLocationType.variable} name={eventLocationType.variable}
className="mt-1 text-sm text-red-500" className="text-error mt-1 text-sm"
as="p" as="p"
/> />
</div> </div>
@@ -208,7 +209,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
</div> </div>
); );
} else { } else {
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>; return <p className="text-default text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
} }
})(); })();
@@ -216,16 +217,16 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}> <Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
<DialogContent> <DialogContent>
<div className="flex flex-row space-x-3"> <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"> <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">
<FiMapPin className="text-primary-600 h-6 w-6" /> <MapPin className="text-emphasis h-6 w-6" />
</div> </div>
<div className="w-full"> <div className="w-full">
<div className="mt-3 text-center sm:mt-0 sm:text-left"> <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")} {t("edit_location")}
</h3> </h3>
{!booking && ( {!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"> <Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
Can&apos;t find the right video app? Visit our Can&apos;t find the right video app? Visit our
<Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video"> <Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video">
@@ -240,8 +241,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
{booking && ( {booking && (
<> <>
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p> <p className="text-emphasis mt-6 mb-2 ml-1 text-sm font-bold">{t("current_location")}:</p>
<p className="mb-2 ml-1 text-sm text-black"> <p className="text-emphasis mb-2 ml-1 text-sm">
{getHumanReadableLocationValue(booking.location, t)} {getHumanReadableLocationValue(booking.location, t)}
</p> </p>
</> </>
@@ -13,7 +13,7 @@ import {
showToast, showToast,
TextArea, TextArea,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiClock } from "@calcom/ui/components/icon"; import { Clock } from "@calcom/ui/components/icon";
interface IRescheduleDialog { interface IRescheduleDialog {
isOpenDialog: boolean; isOpenDialog: boolean;
@@ -44,14 +44,14 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
<DialogContent> <DialogContent>
<div className="flex flex-row space-x-3"> <div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]"> <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>
<div className="pt-1"> <div className="pt-1">
<DialogHeader title={t("send_reschedule_request")} /> <DialogHeader title={t("send_reschedule_request")} />
<p className="text-sm text-gray-500">{t("reschedule_modal_description")}</p> <p className="text-subtle text-sm">{t("reschedule_modal_description")}</p>
<p className="mt-6 mb-2 text-sm font-bold text-black"> <p className="text-emphasis mt-6 mb-2 text-sm font-bold">
{t("reason_for_reschedule_request")} {t("reason_for_reschedule_request")}
<span className="font-normal text-gray-500"> (Optional)</span> <span className="text-subtle font-normal"> (Optional)</span>
</p> </p>
<TextArea <TextArea
data-testid="reschedule_reason" data-testid="reschedule_reason"
+8 -8
View File
@@ -29,15 +29,15 @@ const ErrorDebugPanel: React.FC<{ error: Props["error"]; children?: never }> = (
]; ];
return ( return (
<div className="overflow-hidden bg-white shadow sm:rounded-lg"> <div className="bg-default overflow-hidden shadow sm:rounded-lg">
<div className="border-t border-gray-200 px-4 py-5 sm:p-0"> <div className="border-subtle border-t px-4 py-5 sm:p-0">
<dl className="sm:divide-y sm:divide-gray-200"> <dl className="sm:divide-subtle sm:divide-y">
{debugMap.map(([key, value]) => { {debugMap.map(([key, value]) => {
if (value !== undefined) { if (value !== undefined) {
return ( return (
<div key={key} className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6"> <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> <dt className="text-emphasis text-sm font-bold">{key}</dt>
<dd className="mt-1 text-sm text-black sm:col-span-2 sm:mt-0">{value}</dd> <dd className="text-emphasis mt-1 text-sm sm:col-span-2 sm:mt-0">{value}</dd>
</div> </div>
); );
} }
@@ -53,11 +53,11 @@ export const ErrorPage: React.FC<Props> = (props) => {
return ( 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"> <main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
<div className="text-center"> <div className="text-center">
<p className="text-sm font-semibold uppercase tracking-wide text-black">{statusCode}</p> <p className="text-emphasis text-sm font-semibold uppercase tracking-wide">{statusCode}</p>
<h1 className="mt-2 text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl"> <h1 className="text-emphasis mt-2 text-4xl font-extrabold tracking-tight sm:text-5xl">
{message} {message}
</h1> </h1>
</div> </div>
@@ -58,35 +58,35 @@ const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
className="mb-0" className="mb-0"
/> />
<div className="pt-6 text-sm"> <div className="pt-6 text-sm">
<div className="mb-6 rounded-md bg-gray-100 p-2"> <div className="bg-subtle mb-6 rounded-md p-2">
<h1 className="mb-2 ml-1 font-medium text-gray-900">{t("available_variables")}</h1> <h1 className="text-emphasis mb-2 ml-1 font-medium">{t("available_variables")}</h1>
<div className="mb-2.5 flex font-normal"> <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-subtle ml-1 mr-5 w-28">{`{Event type title}`}</p>
<p className="text-gray-900">{t("event_name_info")}</p> <p className="text-emphasis">{t("event_name_info")}</p>
</div> </div>
<div className="mb-2.5 flex font-normal"> <div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Organiser}`}</p> <p className="text-subtle ml-1 mr-5 w-28">{`{Organiser}`}</p>
<p className="text-gray-900">{t("your_full_name")}</p> <p className="text-emphasis">{t("your_full_name")}</p>
</div> </div>
<div className="mb-2.5 flex font-normal"> <div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Scheduler}`}</p> <p className="text-subtle ml-1 mr-5 w-28">{`{Scheduler}`}</p>
<p className="text-gray-900">{t("scheduler_full_name")}</p> <p className="text-emphasis">{t("scheduler_full_name")}</p>
</div> </div>
<div className="mb-1 flex font-normal"> <div className="mb-1 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Location}`}</p> <p className="text-subtle ml-1 mr-5 w-28">{`{Location}`}</p>
<p className="text-gray-900">{t("location_info")}</p> <p className="text-emphasis">{t("location_info")}</p>
</div> </div>
</div> </div>
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1> <h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
<div <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={{ style={{
backgroundImage: "url(/calendar-preview.svg)", backgroundImage: "url(/calendar-preview.svg)",
}}> }}>
<div className="m-auto flex items-center justify-center self-stretch"> <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"> <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="overflow-hidden text-ellipsis p-1.5 font-medium text-gray-900">{previewText}</p> <p className="text-emphasis overflow-hidden text-ellipsis p-1.5 font-medium">{previewText}</p>
<p className="ml-1.5 text-[10px] font-normal text-gray-600">8 - 10 AM</p> <p className="text-default ml-1.5 text-[10px] font-normal">8 - 10 AM</p>
</div> </div>
</div> </div>
</div> </div>
@@ -8,14 +8,17 @@ import { v5 as uuidv5 } from "uuid";
import type { EventNameObjectType } from "@calcom/core/event"; import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } 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 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 { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants"; import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client"; import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui"; import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
import { FiEdit, FiCopy } from "@calcom/ui/components/icon"; import { Edit, Copy } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController"; import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -56,6 +59,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation); const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`; const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled"); const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const noShowFeeEnabled = eventType.metadata.apps?.stripe?.paymentOption === "HOLD";
useEffect(() => { useEffect(() => {
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id)); !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({ const eventNamePlaceholder = getEventName({
...eventNameObject, ...eventNameObject,
eventName: formMethods.watch("eventName"), eventName: formMethods.watch("eventName"),
}); });
const successRedirectUrlLocked = shouldLockDisableProps("successRedirectUrl");
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
const closeEventNameTip = () => setShowEventNameTip(false); const closeEventNameTip = () => setShowEventNameTip(false);
const setEventName = (value: string) => formMethods.setValue("eventName", value); const setEventName = (value: string) => formMethods.setValue("eventName", value);
@@ -99,7 +111,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Link <Link
href="/apps/categories/calendar" href="/apps/categories/calendar"
target="_blank" target="_blank"
className="text-sm text-gray-600 hover:text-gray-900"> className="hover:text-emphasis text-default text-sm">
{t("add_another_calendar")} {t("add_another_calendar")}
</Link> </Link>
</div> </div>
@@ -118,44 +130,51 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
)} )}
/> />
</div> </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>
)} )}
<div className="w-full"> <div className="w-full">
<TextField <TextField
label={t("event_name_in_calendar")} label={t("event_name_in_calendar")}
type="text" type="text"
{...shouldLockDisableProps("eventName")}
placeholder={eventNamePlaceholder} placeholder={eventNamePlaceholder}
defaultValue={eventType.eventName || ""} defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")} {...formMethods.register("eventName")}
addOnSuffix={ addOnSuffix={
<Button <Button
type="button"
StartIcon={FiEdit}
variant="icon"
color="minimal" color="minimal"
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black" size="sm"
onClick={() => setShowEventNameTip((old) => !old)}
aria-label="edit custom name" 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> </div>
<hr /> <hr className="border-subtle" />
<FormBuilder <FormBuilder
title={t("booking_questions_title")} title={t("booking_questions_title")}
description={t("booking_questions_description")} description={t("booking_questions_description")}
addFieldLabel={t("add_a_booking_question")} addFieldLabel={t("add_a_booking_question")}
formProp="bookingFields" formProp="bookingFields"
{...shouldLockDisableProps("bookingFields")}
dataStore={{
options: {
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
},
}}
/> />
<hr /> <hr className="border-subtle" />
<RequiresConfirmationController <RequiresConfirmationController
eventType={eventType}
seatsEnabled={seatsEnabled} seatsEnabled={seatsEnabled}
metadata={eventType.metadata} metadata={eventType.metadata}
requiresConfirmation={requiresConfirmation} requiresConfirmation={requiresConfirmation}
onRequiresConfirmation={setRequiresConfirmation} onRequiresConfirmation={setRequiresConfirmation}
/> />
<hr /> <hr className="border-subtle" />
<Controller <Controller
name="hideCalendarNotes" name="hideCalendarNotes"
control={formMethods.control} control={formMethods.control}
@@ -163,13 +182,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<SettingsToggle <SettingsToggle
title={t("disable_notes")} title={t("disable_notes")}
{...shouldLockDisableProps("hideCalendarNotes")}
description={t("disable_notes_description")} description={t("disable_notes_description")}
checked={value} checked={value}
onCheckedChange={(e) => onChange(e)} onCheckedChange={(e) => onChange(e)}
/> />
)} )}
/> />
<hr /> <hr className="border-subtle" />
<Controller <Controller
name="successRedirectUrl" name="successRedirectUrl"
control={formMethods.control} control={formMethods.control}
@@ -177,48 +197,43 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<> <>
<SettingsToggle <SettingsToggle
title={t("redirect_success_booking")} title={t("redirect_success_booking")}
{...successRedirectUrlLocked}
description={t("redirect_url_description")} description={t("redirect_url_description")}
checked={redirectUrlVisible} checked={redirectUrlVisible}
onCheckedChange={(e) => { onCheckedChange={(e) => {
setRedirectUrlVisible(e); setRedirectUrlVisible(e);
onChange(e ? value : ""); onChange(e ? value : "");
}}> }}>
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */} {/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
<div className="lg:-ml-2"> <div className="lg:-ml-2 lg:-mb-2">
<TextField <TextField
className="w-full"
label={t("redirect_success_booking")} label={t("redirect_success_booking")}
labelSrOnly labelSrOnly
disabled={successRedirectUrlLocked.disabled}
placeholder={t("external_redirect_url")} placeholder={t("external_redirect_url")}
required={redirectUrlVisible} required={redirectUrlVisible}
type="text" type="text"
defaultValue={eventType.successRedirectUrl || ""} defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")} {...formMethods.register("successRedirectUrl")}
/> />
<div className="mt-2 flex"> <div
<Checkbox className={classNames(
description={t("disable_success_page")} "p-1 text-sm text-orange-600",
// Disable if it's not Self Hosted or if the redirect url is not set formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")} )}>
{...formMethods.register("metadata.disableSuccessPage")} {t("redirect_url_warning")}
/>
{/*TODO: Extract it out into a component when used more than once*/}
{!IS_SELF_HOSTED && (
<Link href="https://cal.com/pricing" target="_blank">
<Badge variant="orange" className="ml-2">
Platform Only
</Badge>
</Link>
)}
</div> </div>
</div> </div>
</SettingsToggle> </SettingsToggle>
</> </>
)} )}
/> />
<hr /> <hr className="border-subtle" />
<SettingsToggle <SettingsToggle
data-testid="hashedLinkCheck" data-testid="hashedLinkCheck"
title={t("private_link")} title={t("private_link")}
{...shouldLockDisableProps("hashedLinkCheck")}
description={t("private_link_description", { appName: APP_NAME })} description={t("private_link_description", { appName: APP_NAME })}
checked={hashedLinkVisible} checked={hashedLinkVisible}
onCheckedChange={(e) => { 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")}> <Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Button <Button
color="minimal" 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={() => { onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink); navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) { if (eventType.hashedLink) {
@@ -247,70 +266,76 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
} else { } else {
showToast(t("enabled_after_update_description"), "warning"); showToast(t("enabled_after_update_description"), "warning");
} }
}} }}>
className="hover:stroke-3 hover:bg-transparent hover:text-black" <Copy className="h-4 w-4" />
type="button">
<FiCopy />
</Button> </Button>
</Tooltip> </Tooltip>
} }
/> />
</div> </div>
</SettingsToggle> </SettingsToggle>
<hr /> <hr className="border-subtle" />
<Controller <Controller
name="seatsPerTimeSlotEnabled" name="seatsPerTimeSlotEnabled"
control={formMethods.control} control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot} defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<SettingsToggle <>
data-testid="offer-seats-toggle" <SettingsToggle
title={t("offer_seats")} data-testid="offer-seats-toggle"
description={t("offer_seats_description")} title={t("offer_seats")}
checked={value} {...seatsLocked}
onCheckedChange={(e) => { description={t("offer_seats_description")}
// Enabling seats will disable guests and requiring confirmation until fully supported checked={value}
if (e) { disabled={noShowFeeEnabled}
toggleGuests(false); onCheckedChange={(e) => {
formMethods.setValue("requiresConfirmation", false); // Enabling seats will disable guests and requiring confirmation until fully supported
setRequiresConfirmation(false); if (e) {
formMethods.setValue("seatsPerTimeSlot", 2); toggleGuests(false);
} else { formMethods.setValue("requiresConfirmation", false);
formMethods.setValue("seatsPerTimeSlot", null); setRequiresConfirmation(false);
toggleGuests(true); formMethods.setValue("seatsPerTimeSlot", 2);
} } else {
onChange(e); formMethods.setValue("seatsPerTimeSlot", null);
}}> toggleGuests(true);
<Controller }
name="seatsPerTimeSlot" onChange(e);
control={formMethods.control} }}>
defaultValue={eventType.seatsPerTimeSlot} <Controller
render={({ field: { value, onChange } }) => ( name="seatsPerTimeSlot"
<div className="lg:-ml-2"> control={formMethods.control}
<TextField defaultValue={eventType.seatsPerTimeSlot}
required render={({ field: { value, onChange } }) => (
name="seatsPerTimeSlot" <div className="lg:-ml-2">
labelSrOnly <TextField
label={t("number_of_seats")} required
type="number" name="seatsPerTimeSlot"
defaultValue={value || 2} labelSrOnly
min={1} label={t("number_of_seats")}
addOnSuffix={<>{t("seats")}</>} type="number"
onChange={(e) => { disabled={seatsLocked.disabled}
onChange(Math.abs(Number(e.target.value))); defaultValue={value || 2}
}} min={1}
/> className="w-24"
<div className="mt-2"> addOnSuffix={<>{t("seats")}</>}
<Checkbox onChange={(e) => {
description={t("show_attendees")} onChange(Math.abs(Number(e.target.value)));
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)} }}
defaultChecked={!!eventType.seatsShowAttendees}
/> />
<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>
</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 { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types"; import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
import type { EventTypeAppsList } from "@calcom/app-store/utils"; 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 { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, EmptyScreen } from "@calcom/ui"; import { Button, EmptyScreen, Alert } from "@calcom/ui";
import { FiGrid } from "@calcom/ui/components/icon"; import { Grid, Lock } from "@calcom/ui/components/icon";
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] & export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
EventTypeAppCardComponentProps["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 ( return (
<> <>
<div> <div>
<div className="before:border-0"> <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 ? ( {!isLoading && !installedApps?.length ? (
<EmptyScreen <EmptyScreen
Icon={FiGrid} Icon={Grid}
headline={t("empty_installed_apps_headline")} headline={t("empty_installed_apps_headline")}
description={t("empty_installed_apps_description")} description={t("empty_installed_apps_description")}
buttonRaw={ buttonRaw={
<Button target="_blank" color="secondary" href="/apps"> isChildrenManagedEventType && !isManagedEventType ? (
{t("empty_installed_apps_button")}{" "} <Button StartIcon={Lock} color="secondary" disabled>
</Button> {t("locked_by_admin")}
</Button>
) : (
<Button target="_blank" color="secondary" href="/apps">
{t("empty_installed_apps_button")}{" "}
</Button>
)
} }
/> />
) : null} ) : null}
@@ -82,22 +103,24 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
))} ))}
</div> </div>
</div> </div>
<div> {!shouldLockDisableProps("apps").disabled && (
{!isLoading && notInstalledApps?.length ? ( <div>
<h2 className="mt-0 mb-2 text-lg font-semibold text-gray-900">Available Apps</h2> {!isLoading && notInstalledApps?.length ? (
) : null} <h2 className="text-emphasis mt-0 mb-2 text-lg font-semibold">{t("available_apps")}</h2>
<div className="before:border-0"> ) : null}
{notInstalledApps?.map((app) => ( <div className="before:border-0">
<EventTypeAppCard {notInstalledApps?.map((app) => (
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)} <EventTypeAppCard
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)} getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
key={app.slug} setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
app={app} key={app.slug}
eventType={eventType} app={app}
/> eventType={eventType}
))} />
))}
</div>
</div> </div>
</div> )}
</> </>
); );
}; };
@@ -1,29 +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 { Controller, useFormContext } from "react-hook-form";
import type { OptionProps, SingleValueProps } from "react-select"; import type { OptionProps, SingleValueProps } from "react-select";
import { components } from "react-select"; import { components } from "react-select";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { NewScheduleButton } from "@calcom/features/schedules"; import { NewScheduleButton } from "@calcom/features/schedules";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday"; import { weekdayNames } from "@calcom/lib/weekday";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui"; import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
import { FiExternalLink, FiGlobe, FiClock } from "@calcom/ui/components/icon"; import { ExternalLink, Globe, Clock } from "@calcom/ui/components/icon";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
type AvailabilityOption = { type AvailabilityOption = {
label: string; label: string;
value: number; value: number;
isDefault: boolean; isDefault: boolean;
isManaged?: boolean;
}; };
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => { const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
const { label, isDefault } = props.data; const { label, isDefault, isManaged = false } = props.data;
const { t } = useLocale(); const { t } = useLocale();
return ( return (
<components.Option {...props}> <components.Option {...props}>
@@ -33,12 +33,17 @@ const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
{t("default")} {t("default")}
</Badge> </Badge>
)} )}
{isManaged && (
<Badge variant="gray" className="ml-2">
{t("managed")}
</Badge>
)}
</components.Option> </components.Option>
); );
}; };
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => { const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
const { label, isDefault } = props.data; const { label, isDefault, isManaged = false } = props.data;
const { t } = useLocale(); const { t } = useLocale();
return ( return (
<components.SingleValue {...props}> <components.SingleValue {...props}>
@@ -48,50 +53,39 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
{t("default")} {t("default")}
</Badge> </Badge>
)} )}
{isManaged && (
<Badge variant="gray" className="ml-2">
{t("managed")}
</Badge>
)}
</components.SingleValue> </components.SingleValue>
); );
}; };
const AvailabilitySelect = ({ const AvailabilitySelect = ({
className = "", className = "",
isLoading, options,
schedules, value,
...props ...props
}: { }: {
className?: string; className?: string;
name: string; name: string;
value: number; value: AvailabilityOption | undefined;
isLoading: boolean; options: AvailabilityOption[];
schedules: RouterOutputs["viewer"]["availability"]["list"]["schedules"] | []; isDisabled?: boolean;
onBlur: () => void; onBlur: () => void;
onChange: (value: AvailabilityOption | null) => void; onChange: (value: AvailabilityOption | null) => void;
}) => { }) => {
const { t } = useLocale(); const { t } = useLocale();
if (isLoading) {
return <SelectSkeletonLoader />;
}
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 ( return (
<Select <Select
placeholder={t("select")} placeholder={t("select")}
options={options} options={options}
isDisabled={props.isDisabled}
isSearchable={false} isSearchable={false}
onChange={props.onChange} onChange={props.onChange}
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)} className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
value={value} defaultValue={value}
components={{ Option, SingleValue }} components={{ Option, SingleValue }}
isMulti={false} isMulti={false}
/> />
@@ -103,24 +97,32 @@ const format = (date: Date, hour12: boolean) =>
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")) 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 { data: loggedInUser } = useMeQuery();
const timeFormat = loggedInUser?.timeFormat; const timeFormat = loggedInUser?.timeFormat;
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
const { watch } = useFormContext<FormValues>(); const { watch } = useFormContext<FormValues>();
const scheduleId = watch("schedule"); const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery( const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined }, {
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId } scheduleId: scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
isManagedEventType,
},
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
); );
const filterDays = (dayNum: number) => const filterDays = (dayNum: number) =>
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || []; schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
return ( 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"> <ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => { {weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length; const isAvailable = !!filterDays(index).length;
@@ -128,8 +130,8 @@ const EventTypeScheduleDetails = () => {
<li key={day} className="my-6 flex border-transparent last:mb-2"> <li key={day} className="my-6 flex border-transparent last:mb-2">
<span <span
className={classNames( className={classNames(
"w-20 font-medium sm:w-32", "w-20 font-medium sm:w-32 ",
!isAvailable && "text-gray-500 opacity-50" !isAvailable ? "text-subtle line-through" : "text-default"
)}> )}>
{day} {day}
</span> </span>
@@ -138,34 +140,34 @@ const EventTypeScheduleDetails = () => {
) : isAvailable ? ( ) : isAvailable ? (
<div className="space-y-3 text-right"> <div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => ( {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"> <span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)} {format(dayRange.startTime, timeFormat === 12)}
</span> </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 className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
</div> </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> </li>
); );
})} })}
</ol> </ol>
<hr /> <hr className="border-subtle" />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between"> <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"> <span className="text-default flex items-center justify-center text-sm sm:justify-start">
<FiGlobe className="ltr:mr-2 rtl:ml-2" /> <Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />} {schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span> </span>
{!!schedule?.id && ( {!!schedule?.id && !schedule.isManaged && (
<Button <Button
href={`/availability/${schedule.id}`} href={`/availability/${schedule.id}`}
disabled={isLoading} disabled={isLoading}
color="minimal" color="minimal"
EndIcon={FiExternalLink} EndIcon={ExternalLink}
target="_blank" target="_blank"
rel="noopener noreferrer"> rel="noopener noreferrer">
{t("edit_availability")} {t("edit_availability")}
@@ -176,14 +178,23 @@ const EventTypeScheduleDetails = () => {
); );
}; };
const EventTypeSchedule = () => { const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale(); const { t } = useLocale();
const { data: schedules, isLoading } = trpc.viewer.availability.list.useQuery(); 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");
if (!schedules?.schedules.length && !isLoading) const { data, isLoading } = trpc.viewer.availability.list.useQuery();
if (!data?.schedules.length && !isLoading)
return ( return (
<EmptyScreen <EmptyScreen
Icon={FiClock} Icon={Clock}
headline={t("new_schedule_heading")} headline={t("new_schedule_heading")}
description={t("new_schedule_description")} description={t("new_schedule_description")}
buttonRaw={<NewScheduleButton fromEventType />} buttonRaw={<NewScheduleButton fromEventType />}
@@ -191,22 +202,65 @@ const EventTypeSchedule = () => {
/> />
); );
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 ( return (
<div className="space-y-4"> <div className="space-y-4">
<div> <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")} {t("availability")}
{shouldLockIndicator("availability")}
</label> </label>
<Controller <Controller
name="schedule" name="schedule"
render={({ field }) => ( render={({ field }) => (
<> <>
<AvailabilitySelect <AvailabilitySelect
value={field.value} value={value}
options={options}
onBlur={field.onBlur} onBlur={field.onBlur}
isDisabled={shouldLockDisableProps("schedule").disabled}
name={field.name} name={field.name}
schedules={schedules?.schedules || []}
isLoading={isLoading}
onChange={(selected) => { onChange={(selected) => {
field.onChange(selected?.value || null); field.onChange(selected?.value || null);
}} }}
@@ -215,12 +269,21 @@ const EventTypeSchedule = () => {
)} )}
/> />
</div> </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> </div>
); );
}; };
const UseCommonScheduleSettingsToggle = () => { const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale(); const { t } = useLocale();
const { resetField, setValue } = useFormContext<FormValues>(); const { resetField, setValue } = useFormContext<FormValues>();
return ( return (
@@ -239,13 +302,23 @@ const UseCommonScheduleSettingsToggle = () => {
}} }}
title={t("choose_common_schedule_team_event")} title={t("choose_common_schedule_team_event")}
description={t("choose_common_schedule_team_event_description")}> description={t("choose_common_schedule_team_event_description")}>
<EventTypeSchedule /> <EventTypeSchedule eventType={eventType} />
</SettingsToggle> </SettingsToggle>
)} )}
/> />
); );
}; };
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => { export const EventAvailabilityTab = ({
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />; eventType,
isTeamEvent,
}: {
eventType: EventTypeSetup;
isTeamEvent: boolean;
}) => {
return isTeamEvent && eventType.schedulingType !== SchedulingType.MANAGED ? (
<UseCommonScheduleSettingsToggle eventType={eventType} />
) : (
<EventTypeSchedule eventType={eventType} />
);
}; };
+104 -47
View File
@@ -7,6 +7,7 @@ import type { UseFormRegisterReturn } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form"; import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { SingleValue } from "react-select"; import type { SingleValue } from "react-select";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import type { DurationType } from "@calcom/lib/convertToNewDurationType"; import type { DurationType } from "@calcom/lib/convertToNewDurationType";
import convertToNewDurationType 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 { useLocale } from "@calcom/lib/hooks/useLocale";
import type { PeriodType } from "@calcom/prisma/client"; import type { PeriodType } from "@calcom/prisma/client";
import type { IntervalLimit } from "@calcom/types/Calendar"; import type { IntervalLimit } from "@calcom/types/Calendar";
import { import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
Button, import { Plus, Trash } from "@calcom/ui/components/icon";
DateRangePicker,
Input,
InputField,
Label,
Select,
SettingsToggle,
TextField,
} from "@calcom/ui";
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
const MinimumBookingNoticeInput = React.forwardRef< const MinimumBookingNoticeInput = React.forwardRef<
HTMLInputElement, HTMLInputElement,
@@ -78,6 +70,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
<div className="w-1/2 md:w-full"> <div className="w-1/2 md:w-full">
<InputField <InputField
required required
disabled={passThroughProps.disabled}
defaultValue={minimumBookingNoticeDisplayValues.value} defaultValue={minimumBookingNoticeDisplayValues.value}
onChange={(e) => onChange={(e) =>
setMinimumBookingNoticeDisplayValues({ setMinimumBookingNoticeDisplayValues({
@@ -95,6 +88,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
</div> </div>
<Select <Select
isSearchable={false} isSearchable={false}
isDisabled={passThroughProps.disabled}
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]" className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
defaultValue={durationTypeOptions.find( defaultValue={durationTypeOptions.find(
(option) => option.value === minimumBookingNoticeDisplayValues.type (option) => option.value === minimumBookingNoticeDisplayValues.type
@@ -146,12 +140,30 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
defaultValue: periodType?.type, 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 ( return (
<div className="space-y-8"> <div className="space-y-8">
<div className="space-y-4 lg: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="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="w-full"> <div className="w-full">
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label> <Label htmlFor="beforeBufferTime">
{t("before_event")}
{shouldLockIndicator("bookingLimits")}
</Label>
<Controller <Controller
name="beforeBufferTime" name="beforeBufferTime"
control={formMethods.control} control={formMethods.control}
@@ -170,6 +182,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return ( return (
<Select <Select
isSearchable={false} isSearchable={false}
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
onChange={(val) => { onChange={(val) => {
if (val) onChange(val.value); if (val) onChange(val.value);
}} }}
@@ -183,7 +196,10 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/> />
</div> </div>
<div className="w-full"> <div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")} </Label> <Label htmlFor="afterBufferTime">
{t("after_event")}
{shouldLockIndicator("bookingLimits")}
</Label>
<Controller <Controller
name="afterBufferTime" name="afterBufferTime"
control={formMethods.control} control={formMethods.control}
@@ -202,6 +218,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return ( return (
<Select <Select
isSearchable={false} isSearchable={false}
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
onChange={(val) => { onChange={(val) => {
if (val) onChange(val.value); if (val) onChange(val.value);
}} }}
@@ -217,11 +234,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</div> </div>
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4"> <div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="w-full"> <div className="w-full">
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label> <Label htmlFor="minimumBookingNotice">
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} /> {t("minimum_booking_notice")}
{shouldLockIndicator("minimumBookingNotice")}
</Label>
<MinimumBookingNoticeInput
disabled={shouldLockDisableProps("minimumBookingNotice").disabled}
{...formMethods.register("minimumBookingNotice")}
/>
</div> </div>
<div className="w-full"> <div className="w-full">
<Label htmlFor="slotInterval">{t("slot_interval")} </Label> <Label htmlFor="slotInterval">
{t("slot_interval")}
{shouldLockIndicator("slotInterval")}
</Label>
<Controller <Controller
name="slotInterval" name="slotInterval"
control={formMethods.control} control={formMethods.control}
@@ -239,6 +265,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return ( return (
<Select <Select
isSearchable={false} isSearchable={false}
isDisabled={shouldLockDisableProps("slotInterval").disabled}
onChange={(val) => { onChange={(val) => {
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null); formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
}} }}
@@ -254,13 +281,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</div> </div>
</div> </div>
</div> </div>
<hr /> <hr className="border-subtle" />
<Controller <Controller
name="bookingLimits" name="bookingLimits"
control={formMethods.control} control={formMethods.control}
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<SettingsToggle <SettingsToggle
title={t("limit_booking_frequency")} title={t("limit_booking_frequency")}
{...bookingLimitsLocked}
description={t("limit_booking_frequency_description")} description={t("limit_booking_frequency_description")}
checked={Object.keys(value ?? {}).length > 0} checked={Object.keys(value ?? {}).length > 0}
onCheckedChange={(active) => { onCheckedChange={(active) => {
@@ -272,11 +300,16 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
formMethods.setValue("bookingLimits", {}); formMethods.setValue("bookingLimits", {});
} }
}}> }}>
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} /> <IntervalLimitsManager
disabled={bookingLimitsLocked.disabled}
propertyName="bookingLimits"
defaultLimit={1}
step={1}
/>
</SettingsToggle> </SettingsToggle>
)} )}
/> />
<hr /> <hr className="border-subtle" />
<Controller <Controller
name="durationLimits" name="durationLimits"
control={formMethods.control} control={formMethods.control}
@@ -284,6 +317,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<SettingsToggle <SettingsToggle
title={t("limit_total_booking_duration")} title={t("limit_total_booking_duration")}
description={t("limit_total_booking_duration_description")} description={t("limit_total_booking_duration_description")}
{...durationLimitsLocked}
checked={Object.keys(value ?? {}).length > 0} checked={Object.keys(value ?? {}).length > 0}
onCheckedChange={(active) => { onCheckedChange={(active) => {
if (active) { if (active) {
@@ -297,13 +331,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<IntervalLimitsManager <IntervalLimitsManager
propertyName="durationLimits" propertyName="durationLimits"
defaultLimit={60} defaultLimit={60}
disabled={durationLimitsLocked.disabled}
step={15} step={15}
textFieldSuffix={t("minutes")} textFieldSuffix={t("minutes")}
/> />
</SettingsToggle> </SettingsToggle>
)} )}
/> />
<hr /> <hr className="border-subtle" />
<Controller <Controller
name="periodType" name="periodType"
control={formMethods.control} control={formMethods.control}
@@ -311,49 +346,64 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<SettingsToggle <SettingsToggle
title={t("limit_future_bookings")} title={t("limit_future_bookings")}
description={t("limit_future_bookings_description")} description={t("limit_future_bookings_description")}
{...periodTypeLocked}
checked={value && value !== "UNLIMITED"} checked={value && value !== "UNLIMITED"}
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}> onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
<RadioGroup.Root <RadioGroup.Root
defaultValue={watchPeriodType} defaultValue={watchPeriodType}
value={watchPeriodType} value={watchPeriodType}
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}> 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; if (period.type === "UNLIMITED") return null;
return ( return (
<div <div
className={classNames( 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" watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
)} )}
key={period.type}> key={period.type}>
<RadioGroup.Item {!periodTypeLocked.disabled && (
id={period.type} <RadioGroup.Item
value={period.type} id={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"> value={period.type}
<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" /> 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.Item> <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.prefix ? <span>{period.prefix}&nbsp;</span> : null}
{period.type === "ROLLING" && ( {period.type === "ROLLING" && (
<div className="flex h-9"> <div className="flex items-center">
<Input <TextField
labelSrOnly
type="number" 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" placeholder="30"
disabled={periodTypeLocked.disabled}
{...formMethods.register("periodDays", { valueAsNumber: true })} {...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30} defaultValue={eventType.periodDays || 30}
/> />
<select <Select
id="" options={optionsPeriod}
className="block h-9 w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none" isSearchable={false}
{...formMethods.register("periodCountCalendarDays")} isDisabled={periodTypeLocked.disabled}
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}> onChange={(opt) => {
<option value="1">{t("calendar_days")}</option> formMethods.setValue(
<option value="0">{t("business_days")}</option> "periodCountCalendarDays",
</select> opt?.value.toString() as "0" | "1"
);
}}
defaultValue={
optionsPeriod.find(
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
) ?? optionsPeriod[0]
}
/>
</div> </div>
)} )}
{period.type === "RANGE" && ( {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 <Controller
name="periodDates" name="periodDates"
control={formMethods.control} control={formMethods.control}
@@ -362,6 +412,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<DateRangePicker <DateRangePicker
startDate={formMethods.getValues("periodDates").startDate} startDate={formMethods.getValues("periodDates").startDate}
endDate={formMethods.getValues("periodDates").endDate} endDate={formMethods.getValues("periodDates").endDate}
disabled={periodTypeLocked.disabled}
onDatesChange={({ startDate, endDate }) => { onDatesChange={({ startDate, endDate }) => {
formMethods.setValue("periodDates", { formMethods.setValue("periodDates", {
startDate, startDate,
@@ -373,9 +424,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/> />
</div> </div>
)} )}
{period.suffix ? ( {period.suffix ? <span className="ms-2 me-2">&nbsp;{period.suffix}</span> : null}
<span className="ltr:ml-2 ltr:mr-2 rtl:ml-2">&nbsp;{period.suffix}</span>
) : null}
</div> </div>
); );
})} })}
@@ -402,6 +451,7 @@ type IntervalLimitItemProps = {
step: number; step: number;
value: number; value: number;
textFieldSuffix?: string; textFieldSuffix?: string;
disabled?: boolean;
selectOptions: { value: keyof IntervalLimit; label: string }[]; selectOptions: { value: keyof IntervalLimit; label: string }[];
hasDeleteButton?: boolean; hasDeleteButton?: boolean;
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void; onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
@@ -416,6 +466,7 @@ const IntervalLimitItem = ({
textFieldSuffix, textFieldSuffix,
selectOptions, selectOptions,
hasDeleteButton, hasDeleteButton,
disabled,
onDelete, onDelete,
onLimitChange, onLimitChange,
onIntervalSelect, onIntervalSelect,
@@ -426,7 +477,9 @@ const IntervalLimitItem = ({
required required
type="number" type="number"
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"} containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
className="mb-0 !h-auto"
placeholder={`${value}`} placeholder={`${value}`}
disabled={disabled}
min={step} min={step}
step={step} step={step}
defaultValue={value} defaultValue={value}
@@ -436,11 +489,12 @@ const IntervalLimitItem = ({
<Select <Select
options={selectOptions} options={selectOptions}
isSearchable={false} isSearchable={false}
isDisabled={disabled}
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)} defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
onChange={onIntervalSelect} onChange={onIntervalSelect}
/> />
{hasDeleteButton && ( {hasDeleteButton && !disabled && (
<Button variant="icon" StartIcon={FiTrash} color="destructive" onClick={() => onDelete(limitKey)} /> <Button variant="icon" StartIcon={Trash} color="destructive" onClick={() => onDelete(limitKey)} />
)} )}
</div> </div>
); );
@@ -451,6 +505,7 @@ type IntervalLimitsManagerProps<K extends "durationLimits" | "bookingLimits"> =
defaultLimit: number; defaultLimit: number;
step: number; step: number;
textFieldSuffix?: string; textFieldSuffix?: string;
disabled?: boolean;
}; };
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
@@ -458,6 +513,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
defaultLimit, defaultLimit,
step, step,
textFieldSuffix, textFieldSuffix,
disabled,
}: IntervalLimitsManagerProps<K>) => { }: IntervalLimitsManagerProps<K>) => {
const { watch, setValue, control } = useFormContext<FormValues>(); const { watch, setValue, control } = useFormContext<FormValues>();
const watchIntervalLimits = watch(propertyName); const watchIntervalLimits = watch(propertyName);
@@ -507,6 +563,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
limitKey={limitKey} limitKey={limitKey}
step={step} step={step}
value={value} value={value}
disabled={disabled}
textFieldSuffix={textFieldSuffix} textFieldSuffix={textFieldSuffix}
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1} hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
selectOptions={INTERVAL_LIMIT_OPTIONS.filter( selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
@@ -537,8 +594,8 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
/> />
); );
})} })}
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && ( {currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && !disabled && (
<Button color="minimal" StartIcon={FiPlus} onClick={addLimit}> <Button color="minimal" StartIcon={Plus} onClick={addLimit}>
{t("add_limit")} {t("add_limit")}
</Button> </Button>
)} )}
@@ -11,7 +11,7 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
return ( return (
<div className=""> <div className="">
<RecurringEventController paymentEnabled={requirePayment} recurringEvent={eventType.recurringEvent} /> <RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
</div> </div>
); );
}; };
+134 -43
View File
@@ -4,20 +4,31 @@ import { isValidPhoneNumber } from "libphonenumber-js";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import Link from "next/link"; import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form"; import { Controller, useForm, useFormContext } from "react-hook-form";
import type { MultiValue } from "react-select"; import type { MultiValue } from "react-select";
import { z } from "zod"; import { z } from "zod";
import type { EventLocationType } from "@calcom/app-store/locations"; import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, MeetLocationType, LocationType } 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 { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt"; import { md } from "@calcom/lib/markdownIt";
import { slugify } from "@calcom/lib/slugify"; import { slugify } from "@calcom/lib/slugify";
import turndown from "@calcom/lib/turndownService"; import turndown from "@calcom/lib/turndownService";
import { Button, Editor, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui"; import {
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon"; 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 { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect"; import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
@@ -35,6 +46,59 @@ const getLocationFromType = (
} }
}; };
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 = ( export const EventSetupTab = (
props: Pick< props: Pick<
EventTypeSetupProps, EventTypeSetupProps,
@@ -137,6 +201,13 @@ export const EventSetupTab = (
resolver: zodResolver(locationFormSchema), resolver: zodResolver(locationFormSchema),
}); });
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const Locations = () => { const Locations = () => {
const { t } = useLocale(); const { t } = useLocale();
@@ -151,6 +222,12 @@ export const EventSetupTab = (
return true; return true;
}); });
const defaultValue = isManagedEventType
? locationOptions.find((op) => op.label === t("default"))?.options[0]
: undefined;
const { locationDetails, locationAvailable } = getLocationInfo(props);
return ( return (
<div className="w-full"> <div className="w-full">
{validLocations.length === 0 && ( {validLocations.length === 0 && (
@@ -158,6 +235,8 @@ export const EventSetupTab = (
<LocationSelect <LocationSelect
placeholder={t("select")} placeholder={t("select")}
options={locationOptions} options={locationOptions}
isDisabled={shouldLockDisableProps("locations").disabled}
defaultValue={defaultValue}
isSearchable={false} isSearchable={false}
className="block w-full min-w-0 flex-1 rounded-sm text-sm" className="block w-full min-w-0 flex-1 rounded-sm text-sm"
menuPlacement="auto" menuPlacement="auto"
@@ -192,16 +271,8 @@ export const EventSetupTab = (
return ( return (
<li <li
onClick={() => {
locationFormMethods.setValue("locationType", location.type);
locationFormMethods.unregister("locationLink");
locationFormMethods.unregister("locationAddress");
locationFormMethods.unregister("locationPhoneNumber");
setEditingLocationType(location.type);
openLocationModal(location.type);
}}
key={`${location.type}${index}`} key={`${location.type}${index}`}
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer"> className="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 justify-between">
<div className="flex items-center"> <div className="flex items-center">
<img <img
@@ -209,7 +280,7 @@ export const EventSetupTab = (
className="h-4 w-4" className="h-4 w-4"
alt={`${eventLocationType.label} logo`} 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>
<div className="flex"> <div className="flex">
<button <button
@@ -223,11 +294,11 @@ export const EventSetupTab = (
openLocationModal(location.type); openLocationModal(location.type);
}} }}
aria-label={t("edit")} aria-label={t("edit")}
className="mr-1 p-1 text-gray-500 hover:text-gray-900"> className="hover:text-emphasis text-subtle mr-1 p-1">
<FiEdit2 className="h-4 w-4" /> <Edit2 className="h-4 w-4" />
</button> </button>
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}> <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> </button>
</div> </div>
</div> </div>
@@ -238,8 +309,8 @@ export const EventSetupTab = (
(location) => (location) =>
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar" location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
) && ( ) && (
<div className="flex text-sm text-gray-600"> <div className="text-default flex text-sm">
<FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" /> <Check className="mt-0.5 mr-1.5 h-2 w-2.5" />
<Trans i18nKey="event_type_requres_google_cal"> <Trans i18nKey="event_type_requres_google_cal">
<p> <p>
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work. The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
@@ -253,11 +324,19 @@ export const EventSetupTab = (
</Trans> </Trans>
</div> </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> <li>
<Button <Button
data-testid="add-location" data-testid="add-location"
StartIcon={FiPlus} StartIcon={Plus}
color="minimal" color="minimal"
onClick={() => setShowLocationModal(true)}> onClick={() => setShowLocationModal(true)}>
{t("add_location")} {t("add_location")}
@@ -270,27 +349,33 @@ export const EventSetupTab = (
); );
}; };
const lengthLockedProps = shouldLockDisableProps("length");
const descriptionLockedProps = shouldLockDisableProps("description");
return ( return (
<div> <div>
<div className="space-y-8"> <div className="space-y-8">
<TextField <TextField
required required
label={t("title")} label={t("title")}
{...shouldLockDisableProps("title")}
defaultValue={eventType.title} defaultValue={eventType.title}
{...formMethods.register("title")} {...formMethods.register("title")}
/> />
<div> <div>
<Label>{t("description")}</Label> <Label>
<Editor {t("description")}
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")} {shouldLockIndicator("description")}
setText={(value: string) => formMethods.setValue("description", turndown(value))} </Label>
excludedToolbarItems={["blockType"]} <DescriptionEditor
placeholder={t("quick_video_meeting")} description={eventType?.description}
editable={!descriptionLockedProps.disabled}
/> />
</div> </div>
<TextField <TextField
required required
label={t("URL")} label={t("URL")}
{...shouldLockDisableProps("slug")}
defaultValue={eventType.slug} defaultValue={eventType.slug}
addOnLeading={ addOnLeading={
<> <>
@@ -343,12 +428,14 @@ export const EventSetupTab = (
<div> <div>
<Skeleton as={Label} loadingClassName="w-16"> <Skeleton as={Label} loadingClassName="w-16">
{t("default_duration")} {t("default_duration")}
{shouldLockIndicator("length")}
</Skeleton> </Skeleton>
<Select <Select
value={defaultDuration} value={defaultDuration}
isSearchable={false} isSearchable={false}
name="length" name="length"
className="text-sm" className="text-sm"
isDisabled={lengthLockedProps.disabled}
noOptionsMessage={() => t("default_duration_no_options")} noOptionsMessage={() => t("default_duration_no_options")}
options={selectedMultipleDuration} options={selectedMultipleDuration}
onChange={(option) => { onChange={(option) => {
@@ -364,32 +451,36 @@ export const EventSetupTab = (
<TextField <TextField
required required
type="number" type="number"
{...lengthLockedProps}
label={t("duration")} label={t("duration")}
defaultValue={eventType.length ?? 15} defaultValue={eventType.length ?? 15}
{...formMethods.register("length")} {...formMethods.register("length")}
addOnSuffix={<>{t("minutes")}</>} addOnSuffix={<>{t("minutes")}</>}
/> />
)} )}
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal"> {!lengthLockedProps.disabled && (
<SettingsToggle <div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
title={t("allow_booker_to_select_duration")} <SettingsToggle
checked={multipleDuration !== undefined} title={t("allow_booker_to_select_duration")}
onCheckedChange={() => { checked={multipleDuration !== undefined}
if (multipleDuration !== undefined) { onCheckedChange={() => {
setMultipleDuration(undefined); if (multipleDuration !== undefined) {
formMethods.setValue("metadata.multipleDuration", undefined); setMultipleDuration(undefined);
formMethods.setValue("length", eventType.length); formMethods.setValue("metadata.multipleDuration", undefined);
} else { formMethods.setValue("length", eventType.length);
setMultipleDuration([]); } else {
formMethods.setValue("metadata.multipleDuration", []); setMultipleDuration([]);
formMethods.setValue("length", 0); formMethods.setValue("metadata.multipleDuration", []);
} formMethods.setValue("length", 0);
}} }
/> }}
</div> />
</div>
)}
<div> <div>
<Skeleton as={Label} loadingClassName="w-16"> <Skeleton as={Label} loadingClassName="w-16">
{t("location")} {t("location")}
{shouldLockIndicator("locations")}
</Skeleton> </Skeleton>
<Controller <Controller
+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 type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import type { ComponentProps } 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 type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import CheckedTeamSelect 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 { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label, Select } from "@calcom/ui"; import { Label, Select } from "@calcom/ui";
interface IMemberToValue { interface IUserToValue {
id: number | null; id: number | null;
name: string | null; name: string | null;
username: string | null; username: string | null;
email: string; email: string;
} }
const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({ const mapUserToValue = ({ id, name, username, email }: IUserToValue) => ({
value: `${id || ""}`, value: `${id || ""}`,
label: `${name || ""}`, label: `${name || ""}`,
avatar: `${WEBAPP_URL}/${username}/avatar.png`, avatar: `${WEBAPP_URL}/${username}/avatar.png`,
email, 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>) => { const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
if (a.label < b.label) { if (a.label < b.label) {
return -1; return -1;
@@ -35,6 +57,40 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
return 0; 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 = ({ const CheckedHostField = ({
labelText, labelText,
placeholder, placeholder,
@@ -52,7 +108,7 @@ const CheckedHostField = ({
options?: Options<CheckedSelectOption>; options?: Options<CheckedSelectOption>;
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => { } & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
return ( 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> <div>
<Label>{labelText}</Label> <Label>{labelText}</Label>
<CheckedTeamSelect <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 = ({ const Hosts = ({
teamMembers, teamMembers,
}: { }: {
@@ -189,6 +260,7 @@ const Hosts = ({
/>*/} />*/}
</> </>
), ),
MANAGED: <></>,
}; };
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>; 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 { t } = useLocale();
const schedulingTypeOptions: { const schedulingTypeOptions: {
@@ -216,9 +292,13 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
}, },
]; ];
const teamMembersOptions = teamMembers.map(mapUserToValue); const teamMembersOptions = teamMembers.map(mapUserToValue);
const childrenEventTypeOptions = teamMembers.map((member) => {
return mapMemberToChildrenOption(member, eventType.slug);
});
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
return ( return (
<div> <div>
{team && ( {team && !isManagedEventType && (
<div className="space-y-5"> <div className="space-y-5">
<div className="flex flex-col"> <div className="flex flex-col">
<Label>{t("scheduling_type")}</Label> <Label>{t("scheduling_type")}</Label>
@@ -239,6 +319,9 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
<Hosts teamMembers={teamMembersOptions} /> <Hosts teamMembers={teamMembersOptions} />
</div> </div>
)} )}
{team && isManagedEventType && (
<ChildrenEventTypes childrenEventTypeOptions={childrenEventTypeOptions} />
)}
</div> </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 type { EventTypeSetupProps } from "pages/event-types/[type]";
import { useState } from "react"; 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 { WebhookForm } from "@calcom/features/webhooks/components";
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm"; import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem"; import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
import { APP_NAME } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Webhook } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui"; import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon"; import { Plus, Lock } from "@calcom/ui/components/icon";
export const EventTeamWebhooksTab = ({ export const EventTeamWebhooksTab = ({
eventType, eventType,
@@ -85,12 +86,20 @@ export const EventTeamWebhooksTab = ({
<Button <Button
color="secondary" color="secondary"
data-testid="new_webhook" data-testid="new_webhook"
StartIcon={FiPlus} StartIcon={Plus}
onClick={() => setCreateModalOpen(true)}> onClick={() => setCreateModalOpen(true)}>
{t("new_webhook")} {t("new_webhook")}
</Button> </Button>
); );
}; };
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const webhookLockedStatus = shouldLockDisableProps("webhooks");
return ( return (
<div> <div>
{team && webhooks && !isLoading && ( {team && webhooks && !isLoading && (
@@ -98,15 +107,24 @@ export const EventTeamWebhooksTab = ({
<div> <div>
<div> <div>
<> <>
{isManagedEventType && (
<Alert
severity="neutral"
className="mb-2"
title={t("locked_for_members")}
message={t("locked_webhooks_description")}
/>
)}
{webhooks.length ? ( {webhooks.length ? (
<> <>
<div className="mb-8 rounded-md border"> <div className="mb-2 rounded-md border">
{webhooks.map((webhook, index) => { {webhooks.map((webhook, index) => {
return ( return (
<WebhookListItem <WebhookListItem
key={webhook.id} key={webhook.id}
webhook={webhook} webhook={webhook}
lastItem={webhooks.length === index + 1} lastItem={webhooks.length === index + 1}
canEditWebhook={!webhookLockedStatus.disabled}
onEditWebhook={() => { onEditWebhook={() => {
setEditModalOpen(true); setEditModalOpen(true);
setWebhookToEdit(webhook); setWebhookToEdit(webhook);
@@ -122,7 +140,15 @@ export const EventTeamWebhooksTab = ({
Icon={TbWebhook} Icon={TbWebhook}
headline={t("create_your_first_webhook")} headline={t("create_your_first_webhook")}
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })} 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,10 +1,13 @@
import { SchedulingType } from "@prisma/client";
import { Webhook as TbWebhook } from "lucide-react";
import type { TFunction } from "next-i18next"; import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useState, Suspense } from "react"; import { useMemo, useState, Suspense } from "react";
import type { UseFormReturn } from "react-hook-form"; 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 Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
@@ -32,19 +35,19 @@ import {
VerticalTabs, VerticalTabs,
} from "@calcom/ui"; } from "@calcom/ui";
import { import {
FiLink, Link as LinkIcon,
FiCalendar, Calendar,
FiClock, Clock,
FiSliders, Sliders,
FiRepeat, Repeat,
FiGrid, Grid,
FiZap, Zap,
FiUsers, Users,
FiExternalLink, ExternalLink,
FiCode, Code,
FiTrash, Trash,
FiMoreHorizontal, MoreHorizontal,
FiLoader, Loader,
} from "@calcom/ui/components/icon"; } from "@calcom/ui/components/icon";
import { EmbedButton, EmbedDialog } from "@components/Embed"; import { EmbedButton, EmbedDialog } from "@components/Embed";
@@ -77,44 +80,38 @@ function getNavigation(props: {
{ {
name: "event_setup_tab_title", name: "event_setup_tab_title",
href: `/event-types/${eventType.id}?tabName=setup`, href: `/event-types/${eventType.id}?tabName=setup`,
icon: FiLink, icon: LinkIcon,
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props 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", name: "event_limit_tab_title",
href: `/event-types/${eventType.id}?tabName=limits`, href: `/event-types/${eventType.id}?tabName=limits`,
icon: FiClock, icon: Clock,
info: `event_limit_tab_description`, info: `event_limit_tab_description`,
}, },
{ {
name: "event_advanced_tab_title", name: "event_advanced_tab_title",
href: `/event-types/${eventType.id}?tabName=advanced`, href: `/event-types/${eventType.id}?tabName=advanced`,
icon: FiSliders, icon: Sliders,
info: `event_advanced_tab_description`, info: `event_advanced_tab_description`,
}, },
{ {
name: "recurring", name: "recurring",
href: `/event-types/${eventType.id}?tabName=recurring`, href: `/event-types/${eventType.id}?tabName=recurring`,
icon: FiRepeat, icon: Repeat,
info: `recurring_event_tab_description`, info: `recurring_event_tab_description`,
}, },
{ {
name: "apps", name: "apps",
href: `/event-types/${eventType.id}?tabName=apps`, href: `/event-types/${eventType.id}?tabName=apps`,
icon: FiGrid, icon: Grid,
//TODO: Handle proper translation with count handling //TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`, info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
}, },
{ {
name: "workflows", name: "workflows",
href: `/event-types/${eventType.id}?tabName=workflows`, href: `/event-types/${eventType.id}?tabName=workflows`,
icon: FiZap, icon: Zap,
info: `${enabledWorkflowsNumber} ${t("active")}`, info: `${enabledWorkflowsNumber} ${t("active")}`,
}, },
]; ];
@@ -137,7 +134,10 @@ function EventTypeSingleLayout({
const router = useRouter(); const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); 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({ const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => { 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 // Define tab navigation here
const EventTypeTabs = useMemo(() => { const EventTypeTabs = useMemo(() => {
const navigation = getNavigation({ let navigation = getNavigation({
t, t,
eventType, eventType,
enabledAppsNumber, enabledAppsNumber,
installedAppsNumber, installedAppsNumber,
enabledWorkflowsNumber, 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 there is a team put this navigation item within the tabs
if (team) { if (team) {
navigation.splice(2, 0, { navigation.splice(2, 0, {
name: "assignment", name: "assignment",
href: `/event-types/${eventType.id}?tabName=team`, href: `/event-types/${eventType.id}?tabName=team`,
icon: FiUsers, icon: Users,
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin", 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({ navigation.push({
name: "webhooks", name: "webhooks",
href: `/event-types/${eventType.id}?tabName=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 embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
return ( return (
<Shell <Shell
@@ -197,86 +230,96 @@ function EventTypeSingleLayout({
heading={eventType.title} heading={eventType.title}
CTA={ CTA={
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<div className="hidden items-center rounded-md px-2 sm:hover:bg-gray-100 lg:flex"> {!eventType.metadata.managedEventConfig && (
<Skeleton <>
as={Label} <div className="sm:hover:bg-subtle hidden items-center rounded-md px-2 lg:flex">
htmlFor="hiddenSwitch" <Skeleton
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline"> as={Label}
{t("hide_from_profile")} htmlFor="hiddenSwitch"
</Skeleton> className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
<Switch {t("hide_from_profile")}
id="hiddenSwitch" </Skeleton>
checked={formMethods.watch("hidden")} <Switch
onCheckedChange={(e) => { id="hiddenSwitch"
formMethods.setValue("hidden", e); checked={formMethods.watch("hidden")}
}} onCheckedChange={(e) => {
/> formMethods.setValue("hidden", e);
</div> }}
<VerticalDivider className="hidden lg:block" /> />
</div>
<VerticalDivider className="hidden lg:block" />
</>
)}
{/* TODO: Figure out why combined isnt working - works in storybook */} {/* TODO: Figure out why combined isnt working - works in storybook */}
<ButtonGroup combined containerProps={{ className: "border-gray-300 hidden lg:flex" }}> <ButtonGroup combined containerProps={{ className: "border-default hidden lg:flex" }}>
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */} {!isManagedEventType && (
<Tooltip content={t("preview")}> <>
<Button {/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
color="secondary" <Tooltip content={t("preview")}>
data-testid="preview-button" <Button
target="_blank" color="secondary"
variant="icon" data-testid="preview-button"
href={permalink} target="_blank"
rel="noreferrer" variant="icon"
StartIcon={FiExternalLink} href={permalink}
/> rel="noreferrer"
</Tooltip> StartIcon={ExternalLink}
/>
</Tooltip>
<Button <Button
color="secondary" color="secondary"
variant="icon" variant="icon"
StartIcon={FiLink} StartIcon={LinkIcon}
tooltip={t("copy_link")} tooltip={t("copy_link")}
onClick={() => { onClick={() => {
navigator.clipboard.writeText(permalink); navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success"); showToast("Link copied!", "success");
}} }}
/> />
<EmbedButton <EmbedButton
embedUrl={encodeURIComponent(embedLink)} embedUrl={encodeURIComponent(embedLink)}
StartIcon={FiCode} StartIcon={Code}
color="secondary" color="secondary"
variant="icon" variant="icon"
tooltip={t("embed")} tooltip={t("embed")}
/> />
<Button </>
color="destructive" )}
variant="icon" {!isChildrenManagedEventType && (
StartIcon={FiTrash} <Button
tooltip={t("delete")} color="destructive"
disabled={!hasPermsToDelete} variant="icon"
onClick={() => setDeleteDialogOpen(true)} StartIcon={Trash}
/> tooltip={t("delete")}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}
/>
)}
</ButtonGroup> </ButtonGroup>
<VerticalDivider className="hidden lg:block" /> <VerticalDivider className="hidden lg:block" />
<Dropdown> <Dropdown>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" /> <Button className="lg:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent style={{ minWidth: "200px" }}> <DropdownMenuContent style={{ minWidth: "200px" }}>
<DropdownMenuItem className="focus:ring-gray-100"> <DropdownMenuItem className="focus:ring-muted">
<DropdownItem <DropdownItem
target="_blank" target="_blank"
type="button" type="button"
StartIcon={FiExternalLink} StartIcon={ExternalLink}
href={permalink} href={permalink}
rel="noreferrer"> rel="noreferrer">
{t("preview")} {t("preview")}
</DropdownItem> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100"> <DropdownMenuItem className="focus:ring-muted">
<DropdownItem <DropdownItem
type="button" type="button"
StartIcon={FiLink} StartIcon={LinkIcon}
onClick={() => { onClick={() => {
navigator.clipboard.writeText(permalink); navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success"); showToast("Link copied!", "success");
@@ -284,18 +327,18 @@ function EventTypeSingleLayout({
{t("copy_link")} {t("copy_link")}
</DropdownItem> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100"> <DropdownMenuItem className="focus:ring-muted">
<DropdownItem <DropdownItem
type="button" type="button"
color="destructive" color="destructive"
StartIcon={FiTrash} StartIcon={Trash}
disabled={!hasPermsToDelete} disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}> onClick={() => setDeleteDialogOpen(true)}>
{t("delete")} {t("delete")}
</DropdownItem> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <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 <Skeleton
as={Label} as={Label}
htmlFor="hiddenSwitch" htmlFor="hiddenSwitch"
@@ -312,7 +355,7 @@ function EventTypeSingleLayout({
</div> </div>
</DropdownMenuContent> </DropdownMenuContent>
</Dropdown> </Dropdown>
<div className="border-l-2 border-gray-300" /> <div className="border-default border-l-2" />
<Button <Button
className="ml-4 lg:ml-0" className="ml-4 lg:ml-0"
type="submit" type="submit"
@@ -323,7 +366,7 @@ function EventTypeSingleLayout({
</Button> </Button>
</div> </div>
}> }>
<Suspense fallback={<FiLoader />}> <Suspense fallback={<Loader />}>
<div className="flex flex-col xl:flex-row xl:space-x-6"> <div className="flex flex-col xl:flex-row xl:space-x-6">
<div className="hidden xl:block"> <div className="hidden xl:block">
<VerticalTabs <VerticalTabs
@@ -339,7 +382,7 @@ function EventTypeSingleLayout({
<div className="w-full ltr:mr-2 rtl:ml-2"> <div className="w-full ltr:mr-2 rtl:ml-2">
<div <div
className={classNames( 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" disableBorder ? "border-0 " : "p-2 md:border md:p-6"
)}> )}>
{children} {children}
@@ -351,14 +394,25 @@ function EventTypeSingleLayout({
<ConfirmationDialogContent <ConfirmationDialogContent
isLoading={deleteMutation.isLoading} isLoading={deleteMutation.isLoading}
variety="danger" variety="danger"
title={t("delete_event_type")} title={t(`delete${isManagedEvent}_event_type`)}
confirmBtnText={t("confirm_delete_event_type")} confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t("confirm_delete_event_type")} loadingText={t(`confirm_delete_event_type`)}
onConfirm={(e) => { onConfirm={(e) => {
e.preventDefault(); e.preventDefault();
deleteMutation.mutate({ id: eventType.id }); 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> </ConfirmationDialogContent>
</Dialog> </Dialog>
<EmbedDialog /> <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 { useState } from "react";
import { useFormContext } from "react-hook-form"; 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 { useLocale } from "@calcom/lib/hooks/useLocale";
import { Frequency } from "@calcom/prisma/zod-utils"; import { Frequency } from "@calcom/prisma/zod-utils";
import type { RecurringEvent } from "@calcom/types/Calendar"; import type { RecurringEvent } from "@calcom/types/Calendar";
import { Alert, Select, SettingsToggle } from "@calcom/ui"; import { Alert, Select, SettingsToggle, TextField } from "@calcom/ui";
type RecurringEventControllerProps = { type RecurringEventControllerProps = {
recurringEvent: RecurringEvent | null; eventType: EventTypeSetup;
paymentEnabled: boolean; paymentEnabled: boolean;
}; };
export default function RecurringEventController({ export default function RecurringEventController({
recurringEvent, eventType,
paymentEnabled, paymentEnabled,
}: RecurringEventControllerProps) { }: RecurringEventControllerProps) {
const { t } = useLocale(); const { t } = useLocale();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(recurringEvent); const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
eventType.recurringEvent
);
const formMethods = useFormContext<FormValues>(); const formMethods = useFormContext<FormValues>();
/* Just yearly-0, monthly-1 and weekly-2 */ /* Just yearly-0, monthly-1 and weekly-2 */
@@ -28,6 +31,14 @@ export default function RecurringEventController({
value: value.toString(), value: value.toString(),
})); }));
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const recurringLocked = shouldLockDisableProps("recurringEvent");
return ( return (
<div className="block items-start sm:flex"> <div className="block items-start sm:flex">
<div className={!paymentEnabled ? "w-full" : ""}> <div className={!paymentEnabled ? "w-full" : ""}>
@@ -37,6 +48,7 @@ export default function RecurringEventController({
<> <>
<SettingsToggle <SettingsToggle
title={t("recurring_event")} title={t("recurring_event")}
{...recurringLocked}
description={t("recurring_event_description")} description={t("recurring_event_description")}
checked={recurringEventState !== null} checked={recurringEventState !== null}
data-testid="recurring-event-check" data-testid="recurring-event-check"
@@ -45,7 +57,7 @@ export default function RecurringEventController({
formMethods.setValue("recurringEvent", null); formMethods.setValue("recurringEvent", null);
setRecurringEventState(null); setRecurringEventState(null);
} else { } else {
const newVal = recurringEvent || { const newVal = eventType.recurringEvent || {
interval: 1, interval: 1,
count: 12, count: 12,
freq: Frequency.WEEKLY, freq: Frequency.WEEKLY,
@@ -57,12 +69,13 @@ export default function RecurringEventController({
{recurringEventState && ( {recurringEventState && (
<div data-testid="recurring-event-collapsible" className="text-sm"> <div data-testid="recurring-event-collapsible" className="text-sm">
<div className="flex items-center"> <div className="flex items-center">
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p> <p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
<input <TextField
disabled={recurringLocked.disabled}
type="number" type="number"
min="1" min="1"
max="20" 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} defaultValue={recurringEventState.interval}
onChange={(event) => { onChange={(event) => {
const newVal = { const newVal = {
@@ -77,7 +90,8 @@ export default function RecurringEventController({
options={recurringEventFreqOptions} options={recurringEventFreqOptions}
value={recurringEventFreqOptions[recurringEventState.freq]} value={recurringEventFreqOptions[recurringEventState.freq]}
isSearchable={false} 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) => { onChange={(event) => {
const newVal = { const newVal = {
...recurringEventState, ...recurringEventState,
@@ -89,13 +103,14 @@ export default function RecurringEventController({
/> />
</div> </div>
<div className="mt-4 flex items-center"> <div className="mt-4 flex items-center">
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p> <p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
<input <TextField
disabled={recurringLocked.disabled}
type="number" type="number"
min="1" min="1"
max="20" 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} defaultValue={recurringEventState.count}
className="mb-0"
onChange={(event) => { onChange={(event) => {
const newVal = { const newVal = {
...recurringEventState, ...recurringEventState,
@@ -105,7 +120,7 @@ export default function RecurringEventController({
setRecurringEventState(newVal); 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", { {t("events", {
count: recurringEventState.count, count: recurringEventState.count,
})} })}
@@ -1,25 +1,29 @@
import * as RadioGroup from "@radix-ui/react-radio-group"; import * as RadioGroup from "@radix-ui/react-radio-group";
import type { UnitTypeLongPlural } from "dayjs"; import type { UnitTypeLongPlural } from "dayjs";
import { Trans } from "next-i18next"; 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 type { Dispatch, SetStateAction } from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import type z from "zod"; 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 { useLocale } from "@calcom/lib/hooks/useLocale";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; 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 = { type RequiresConfirmationControllerProps = {
metadata: z.infer<typeof EventTypeMetaDataSchema>; metadata: z.infer<typeof EventTypeMetaDataSchema>;
requiresConfirmation: boolean; requiresConfirmation: boolean;
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>; onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
seatsEnabled: boolean; seatsEnabled: boolean;
eventType: EventTypeSetup;
}; };
export default function RequiresConfirmationController({ export default function RequiresConfirmationController({
metadata, metadata,
eventType,
requiresConfirmation, requiresConfirmation,
onRequiresConfirmation, onRequiresConfirmation,
seatsEnabled, seatsEnabled,
@@ -37,6 +41,23 @@ export default function RequiresConfirmationController({
} }
}, [requiresConfirmation]); }, [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 ( return (
<div className="block items-start sm:flex"> <div className="block items-start sm:flex">
<div className="w-full"> <div className="w-full">
@@ -46,10 +67,11 @@ export default function RequiresConfirmationController({
render={() => ( render={() => (
<SettingsToggle <SettingsToggle
title={t("requires_confirmation")} title={t("requires_confirmation")}
disabled={seatsEnabled} disabled={seatsEnabled || requiresConfirmationLockedProps.disabled}
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined} tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
description={t("requires_confirmation_description")} description={t("requires_confirmation_description")}
checked={requiresConfirmation} checked={requiresConfirmation}
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
onCheckedChange={(val) => { onCheckedChange={(val) => {
formMethods.setValue("requiresConfirmation", val); formMethods.setValue("requiresConfirmation", val);
onRequiresConfirmation(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 flex-col flex-wrap justify-start gap-y-2">
<div className="flex items-center"> {(requiresConfirmationSetup === undefined || !requiresConfirmationLockedProps.disabled) && (
<RadioGroup.Item <RadioField
label={t("always_requires_confirmation")}
disabled={requiresConfirmationLockedProps.disabled}
id="always" id="always"
value="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> {(requiresConfirmationSetup !== undefined || !requiresConfirmationLockedProps.disabled) && (
<Label htmlFor="always" className="!m-0 flex items-center"> <RadioField
{t("always_requires_confirmation")} disabled={requiresConfirmationLockedProps.disabled}
</Label> className="items-center"
</div> label={
<div className="flex items-center"> <>
<RadioGroup.Item <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" id="notice"
value="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> </div>
</RadioGroup.Root> </RadioGroup.Root>
</SettingsToggle> </SettingsToggle>
@@ -1,5 +1,5 @@
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui"; 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() { function SkeletonLoader() {
return ( return (
@@ -11,7 +11,7 @@ function SkeletonLoader() {
<SkeletonText className="h-4 w-24" /> <SkeletonText className="h-4 w-24" />
</div> </div>
</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 /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
@@ -32,11 +32,11 @@ function SkeletonItem() {
<div className=""> <div className="">
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse "> <ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
<li className="flex items-center whitespace-nowrap"> <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" /> <SkeletonText className="h-4 w-12" />
</li> </li>
<li className="flex items-center whitespace-nowrap"> <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" /> <SkeletonText className="h-4 w-12" />
</li> </li>
</ul> </ul>
@@ -6,17 +6,17 @@ import { Button } from "@calcom/ui";
interface ICalendarItem { interface ICalendarItem {
title: string; title: string;
description?: string; description?: string;
imageSrc: string; logo: string;
type: App["type"]; type: App["type"];
} }
const CalendarItem = (props: ICalendarItem) => { const CalendarItem = (props: ICalendarItem) => {
const { title, imageSrc, type } = props; const { title, logo, type } = props;
const { t } = useLocale(); const { t } = useLocale();
return ( return (
<div className="flex flex-row items-center justify-between p-5"> <div className="flex flex-row items-center justify-between p-5">
<div className="flex items-center space-x-3"> <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> <p className="text-sm font-bold">{title}</p>
</div> </div>
<InstallAppButtonWithoutPlanCheck <InstallAppButtonWithoutPlanCheck
@@ -28,14 +28,14 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
<p className="font-sans text-sm font-bold leading-5"> <p className="font-sans text-sm font-bold leading-5">
{name} {name}
{/* Temporarily removed till we use it on another place */} {/* 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")} {t("default")}
</span> */} </span> */}
</p> </p>
<div className="fle-row flex"> <div className="fle-row flex">
<span <span
title={externalId} 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}{" "} {externalId}{" "}
</span> </span>
</div> </div>
@@ -44,11 +44,11 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
{/* <Button {/* <Button
color="minimal" color="minimal"
type="button" 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")} {t("edit")}
</Button> */} </Button> */}
</div> </div>
<div className="h-[1px] w-full border-b border-gray-200" /> <div className="border-subtle h-[1px] w-full border-b" />
<div> <div>
<ul className="p-4"> <ul className="p-4">
{calendars?.map((calendar, i) => ( {calendars?.map((calendar, i) => (
@@ -1,9 +1,8 @@
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterInputs } from "@calcom/trpc/react"; import type { RouterInputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
interface ICreateEventsOnCalendarSelectProps { interface ICreateEventsOnCalendarSelectProps {
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null; calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
} }
@@ -17,7 +16,7 @@ const CreateEventsOnCalendarSelect = (props: ICreateEventsOnCalendarSelectProps)
<> <>
<div className="mt-6 flex flex-row"> <div className="mt-6 flex flex-row">
<div className="w-full"> <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")} {t("create_events_on")}
</label> </label>
<div className="mt-2"> <div className="mt-2">
@@ -31,13 +31,13 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
firstCalendar && firstCalendar &&
firstCalendar.integration && firstCalendar.integration &&
firstCalendar.integration.title && 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 <ConnectedCalendarItem
key={firstCalendar.integration.title} key={firstCalendar.integration.title}
name={firstCalendar.integration.title} name={firstCalendar.integration.title}
logo={firstCalendar.integration.imageSrc} logo={firstCalendar.integration.logo}
externalId={ externalId={
firstCalendar && firstCalendar.calendars && firstCalendar.calendars.length > 0 firstCalendar && firstCalendar.calendars && firstCalendar.calendars.length > 0
? firstCalendar.calendars[0].externalId ? firstCalendar.calendars[0].externalId
@@ -49,22 +49,22 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
</List> </List>
{/* Create event on selected calendar */} {/* Create event on selected calendar */}
<CreateEventsOnCalendarSelect calendar={destinationCalendar} /> <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 */} {/* Connect calendars list */}
{firstCalendar === undefined && queryIntegrations.data && queryIntegrations.data.items.length > 0 && ( {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 &&
queryIntegrations.data.items.map((item) => ( queryIntegrations.data.items.map((item) => (
<li key={item.title}> <li key={item.title}>
{item.title && item.imageSrc && ( {item.title && item.logo && (
<CalendarItem <CalendarItem
type={item.type} type={item.type}
title={item.title} title={item.title}
description={item.description} description={item.description}
imageSrc={item.imageSrc} logo={item.logo}
/> />
)} )}
</li> </li>
@@ -73,7 +73,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
)} )}
{queryIntegrations.isLoading && ( {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) => { {[0, 0, 0, 0].map((_item, index) => {
return ( return (
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}> <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" type="button"
data-testid="save-calendar-button" data-testid="save-calendar-button"
className={classNames( 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" : "" disabledNextButton ? "cursor-not-allowed opacity-20" : ""
)} )}
onClick={() => nextStep()} onClick={() => nextStep()}
@@ -50,7 +50,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions); const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions);
return ( return (
<Form <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} form={availabilityForm}
handleSubmit={async (values) => { handleSubmit={async (values) => {
try { try {
@@ -117,7 +117,7 @@ const UserProfile = (props: IUserProfileProps) => {
name="avatar" name="avatar"
id="avatar" id="avatar"
placeholder="URL" 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} defaultValue={imageSrc}
/> />
<div className="flex items-center px-4"> <div className="flex items-center px-4">
@@ -142,19 +142,19 @@ const UserProfile = (props: IUserProfileProps) => {
</div> </div>
</div> </div>
<fieldset className="mt-8"> <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 <Editor
getText={() => md.render(getValues("bio") || user?.bio || "")} getText={() => md.render(getValues("bio") || user?.bio || "")}
setText={(value: string) => setValue("bio", turndown(value))} setText={(value: string) => setValue("bio", turndown(value))}
excludedToolbarItems={["blockType"]} 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")} {t("few_sentences_about_yourself")}
</p> </p>
</fieldset> </fieldset>
<Button <Button
type="submit" 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")} {t("finish")}
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" /> <ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</Button> </Button>
@@ -73,7 +73,7 @@ const UserSettings = (props: IUserSettingsProps) => {
{/* Full name textfield */} {/* Full name textfield */}
<div className="w-full"> <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")} {t("full_name")}
</label> </label>
<input <input
@@ -85,7 +85,7 @@ const UserSettings = (props: IUserSettingsProps) => {
type="text" type="text"
autoComplete="off" autoComplete="off"
autoCorrect="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 && ( {errors.name && (
<p data-testid="required" className="py-2 text-xs text-red-500"> <p data-testid="required" className="py-2 text-xs text-red-500">
@@ -95,7 +95,7 @@ const UserSettings = (props: IUserSettingsProps) => {
</div> </div>
{/* Timezone select field */} {/* Timezone select field */}
<div className="w-full"> <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")} {t("timezone")}
</label> </label>
@@ -106,7 +106,7 @@ const UserSettings = (props: IUserSettingsProps) => {
className="mt-2 w-full rounded-md text-sm" 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()} {t("current_time")} {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
</p> </p>
</div> </div>
+4 -2
View File
@@ -24,7 +24,9 @@ export default function WizardLayout({
}, [router.asPath]); }, [router.asPath]);
return ( 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> <div>
<Toaster position="bottom-right" /> <Toaster position="bottom-right" />
</div> </div>
@@ -34,7 +36,7 @@ export default function WizardLayout({
<div className="mx-auto sm:max-w-[520px]"> <div className="mx-auto sm:max-w-[520px]">
<header> <header>
<p className="font-cal mb-3 text-[28px] font-medium leading-7">{title}&nbsp;</p> <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> </header>
<Steps maxSteps={maxSteps} currentStep={currentStep} navigateToStep={noop} /> <Steps maxSteps={maxSteps} currentStep={currentStep} navigateToStep={noop} />
</div> </div>
@@ -59,14 +59,14 @@ const ChangePasswordSection = () => {
return ( 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="py-6 lg:pb-5">
<div className="my-3"> <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>
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0"> <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"> <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")} {t("current_password")}
</label> </label>
<div className="mt-1"> <div className="mt-1">
@@ -77,13 +77,13 @@ const ChangePasswordSection = () => {
name="current_password" name="current_password"
id="current_password" id="current_password"
required 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")} placeholder={t("your_old_password")}
/> />
</div> </div>
</div> </div>
<div className="w-full sm:w-1/2"> <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")} {t("new_password")}
</label> </label>
<div className="mt-1"> <div className="mt-1">
@@ -94,7 +94,7 @@ const ChangePasswordSection = () => {
value={newPassword} value={newPassword}
required required
onInput={(e) => setNewPassword(e.currentTarget.value)} 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")} placeholder={t("super_secure_new_password")}
/> />
</div> </div>
@@ -70,10 +70,10 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
<div className="mb-4"> <div className="mb-4">
<PasswordField <PasswordField
labelProps={{ labelProps={{
className: "block text-sm font-medium text-gray-700", className: "block text-sm font-medium text-default",
}} }}
{...form.register("password")} {...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> <Label className="mt-4"> {t("2fa_code")}</Label>
@@ -82,7 +82,7 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
</div> </div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> <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")} {t("disable")}
</Button> </Button>
<Button color="secondary" onClick={onCancel}> <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 className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
<div> <div>
<div className="flex flex-row items-center"> <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")} {t("user_impersonation_heading")}
</h2> </h2>
<Badge className="ml-2 text-xs" variant={!disableImpersonation ? "success" : "gray"}> <Badge className="ml-2 text-xs" variant={!disableImpersonation ? "success" : "gray"}>
{!disableImpersonation ? t("enabled") : t("disabled")} {!disableImpersonation ? t("enabled") : t("disabled")}
</Badge> </Badge>
</div> </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>
<div className="mt-5 sm:mt-0 sm:self-center"> <div className="mt-5 sm:mt-0 sm:self-center">
<Button <Button
@@ -135,7 +135,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
<WithStep step={SetupStep.ConfirmPassword} current={step}> <WithStep step={SetupStep.ConfirmPassword} current={step}>
<form onSubmit={handleSetup}> <form onSubmit={handleSetup}>
<div className="mb-4"> <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")} {t("password")}
</label> </label>
<div className="mt-1"> <div className="mt-1">
@@ -146,7 +146,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
required required
value={password} value={password}
onInput={(e) => setPassword(e.currentTarget.value)} 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> </div>
@@ -178,22 +178,19 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
<WithStep step={SetupStep.ConfirmPassword} current={step}> <WithStep step={SetupStep.ConfirmPassword} current={step}>
<Button <Button
type="submit" type="submit"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2" className="ms-2 me-2"
onClick={handleSetup} onClick={handleSetup}
disabled={password.length === 0 || isSubmitting}> disabled={password.length === 0 || isSubmitting}>
{t("continue")} {t("continue")}
</Button> </Button>
</WithStep> </WithStep>
<WithStep step={SetupStep.DisplayQrCode} current={step}> <WithStep step={SetupStep.DisplayQrCode} current={step}>
<Button <Button type="submit" className="ms-2 me-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
type="submit"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
onClick={() => setStep(SetupStep.EnterTotpCode)}>
{t("continue")} {t("continue")}
</Button> </Button>
</WithStep> </WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}> <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")} {t("enable")}
</Button> </Button>
</WithStep> </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 className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
<div> <div>
<div className="flex flex-row items-center"> <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"}> <Badge className="ml-2 text-xs" variant={enabled ? "success" : "gray"}>
{enabled ? t("enabled") : t("disabled")} {enabled ? t("enabled") : t("disabled")}
</Badge> </Badge>
</div> </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>
<div className="mt-5 sm:mt-0 sm:self-center"> <div className="mt-5 sm:mt-0 sm:self-center">
<Button <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 }) => { const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
return ( return (
<div className="mb-4 sm:flex sm:items-start"> <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"> <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>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <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} {title}
</h3> </h3>
<p className="text-sm text-gray-400">{description}</p> <p className="text-muted text-sm">{description}</p>
</div> </div>
</div> </div>
); );
@@ -3,7 +3,7 @@ import { useMutation } from "@tanstack/react-query";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Badge, showToast, Switch } from "@calcom/ui"; 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: { export function CalendarSwitch(props: {
type: string; type: string;
@@ -75,8 +75,8 @@ export function CalendarSwitch(props: {
/> />
{props.defaultSelected && ( {props.defaultSelected && (
<Badge variant="gray"> <Badge variant="gray">
<div className="flex"> <div className="flex items-center">
<FiArrowLeft className="mr-1" /> {t("adding_events_to")} <ArrowLeft className="text-default mr-1 h-4 w-4" /> {t("adding_events_to")}
</div> </div>
</Badge> </Badge>
)} )}
@@ -77,10 +77,10 @@ const DisableTwoFactorAuthModal = ({
<div className="mb-4"> <div className="mb-4">
<PasswordField <PasswordField
labelProps={{ labelProps={{
className: "block text-sm font-medium text-gray-700", className: "block text-sm font-medium text-default",
}} }}
{...form.register("password")} {...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> <Label className="mt-4"> {t("2fa_code")}</Label>
@@ -90,7 +90,7 @@ const DisableTwoFactorAuthModal = ({
</div> </div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> <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")} {t("disable")}
</Button> </Button>
<Button color="secondary" onClick={onCancel}> <Button color="secondary" onClick={onCancel}>
@@ -171,22 +171,19 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
<WithStep step={SetupStep.ConfirmPassword} current={step}> <WithStep step={SetupStep.ConfirmPassword} current={step}>
<Button <Button
type="submit" type="submit"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2" className="ms-2 me-2"
onClick={handleSetup} onClick={handleSetup}
disabled={password.length === 0 || isSubmitting}> disabled={password.length === 0 || isSubmitting}>
{t("continue")} {t("continue")}
</Button> </Button>
</WithStep> </WithStep>
<WithStep step={SetupStep.DisplayQrCode} current={step}> <WithStep step={SetupStep.DisplayQrCode} current={step}>
<Button <Button type="submit" className="ms-2 me-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
type="submit"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
onClick={() => setStep(SetupStep.EnterTotpCode)}>
{t("continue")} {t("continue")}
</Button> </Button>
</WithStep> </WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}> <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")} {t("enable")}
</Button> </Button>
</WithStep> </WithStep>
+4 -4
View File
@@ -9,7 +9,7 @@ import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { WEBSITE_URL } from "@calcom/lib/constants"; import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui"; import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
import { FiUserCheck } from "@calcom/ui/components/icon"; import { UserCheck } from "@calcom/ui/components/icon";
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => { export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
const { t } = useLocale(); const { t } = useLocale();
@@ -24,7 +24,7 @@ export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser>
props.onSuccess(); props.onSuccess();
}}> }}>
<EmptyScreen <EmptyScreen
Icon={FiUserCheck} Icon={UserCheck}
headline={t("admin_user_created")} headline={t("admin_user_created")}
description={t("admin_user_created_description")} description={t("admin_user_created_description")}
/> />
@@ -111,7 +111,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<Label htmlFor="username" className={classNames(longWebsiteUrl && "mb-0")}> <Label htmlFor="username" className={classNames(longWebsiteUrl && "mb-0")}>
<span className="block">{t("username")}</span> <span className="block">{t("username")}</span>
{longWebsiteUrl && ( {longWebsiteUrl && (
<small className="items-centerpx-3 mt-2 inline-flex rounded-t-md border border-b-0 border-gray-300 bg-gray-100 py-1 px-3 text-gray-500"> <small className="items-centerpx-3 bg-subtle border-default text-subtle mt-2 inline-flex rounded-t-md border border-b-0 py-1 px-3">
{process.env.NEXT_PUBLIC_WEBSITE_URL} {process.env.NEXT_PUBLIC_WEBSITE_URL}
</small> </small>
)} )}
@@ -119,7 +119,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<TextField <TextField
addOnLeading={ addOnLeading={
!longWebsiteUrl && ( !longWebsiteUrl && (
<span className="inline-flex items-center rounded-none px-3 text-sm text-gray-500"> <span className="text-subtle inline-flex items-center rounded-none px-3 text-sm">
{process.env.NEXT_PUBLIC_WEBSITE_URL}/ {process.env.NEXT_PUBLIC_WEBSITE_URL}/
</span> </span>
) )
+8 -8
View File
@@ -37,13 +37,13 @@ const ChooseLicense = (
<RadioGroup.Item value="FREE"> <RadioGroup.Item value="FREE">
<div <div
className={classNames( className={classNames(
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black", "bg-default cursor-pointer space-y-2 rounded-md border p-4 hover:border-black",
value === "FREE" && "ring-2 ring-black" value === "FREE" && "ring-2 ring-black"
)}> )}>
<h2 className="font-cal text-xl text-black">{t("agplv3_license")}</h2> <h2 className="font-cal text-emphasis text-xl">{t("agplv3_license")}</h2>
<p className="font-medium text-green-800">{t("free_license_fee")}</p> <p className="font-medium text-green-800">{t("free_license_fee")}</p>
<p className="text-gray-500">{t("forever_open_and_free")}</p> <p className="text-subtle">{t("forever_open_and_free")}</p>
<ul className="ml-4 list-disc text-left text-xs text-gray-500"> <ul className="text-subtle ml-4 list-disc text-left text-xs">
<li>{t("required_to_keep_your_code_open_source")}</li> <li>{t("required_to_keep_your_code_open_source")}</li>
<li>{t("cannot_repackage_and_resell")}</li> <li>{t("cannot_repackage_and_resell")}</li>
<li>{t("no_enterprise_features")}</li> <li>{t("no_enterprise_features")}</li>
@@ -53,15 +53,15 @@ const ChooseLicense = (
<RadioGroup.Item value="EE"> <RadioGroup.Item value="EE">
<div <div
className={classNames( className={classNames(
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black", "bg-default cursor-pointer space-y-2 rounded-md border p-4 hover:border-black",
value === "EE" && "ring-2 ring-black" value === "EE" && "ring-2 ring-black"
)}> )}>
<h2 className="font-cal text-xl text-black">{t("ee_enterprise_license")}</h2> <h2 className="font-cal text-emphasis text-xl">{t("ee_enterprise_license")}</h2>
<p className="font-medium text-green-800"> <p className="font-medium text-green-800">
{t("enterprise_booking_fee", { enterprise_booking_fee: ENTERPRISE_BOOKING_FEE })} {t("enterprise_booking_fee", { enterprise_booking_fee: ENTERPRISE_BOOKING_FEE })}
</p> </p>
<p className="text-gray-500">{t("enterprise_license_includes")}</p> <p className="text-subtle">{t("enterprise_license_includes")}</p>
<ul className="ml-4 list-disc text-left text-xs text-gray-500"> <ul className="text-subtle ml-4 list-disc text-left text-xs">
<li>{t("no_need_to_keep_your_code_open_source")}</li> <li>{t("no_need_to_keep_your_code_open_source")}</li>
<li>{t("repackage_rebrand_resell")}</li> <li>{t("repackage_rebrand_resell")}</li>
<li>{t("a_vast_suite_of_enterprise_features")}</li> <li>{t("a_vast_suite_of_enterprise_features")}</li>
+10 -10
View File
@@ -10,7 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react"; import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, TextField } from "@calcom/ui"; import { Button, TextField } from "@calcom/ui";
import { FiCheck, FiExternalLink, FiLoader } from "@calcom/ui/components/icon"; import { Check, ExternalLink, Loader } from "@calcom/ui/components/icon";
type EnterpriseLicenseFormValues = { type EnterpriseLicenseFormValues = {
licenseKey: string; licenseKey: string;
@@ -87,18 +87,18 @@ const EnterpriseLicense = (
return ( return (
<FormProvider {...formMethods}> <FormProvider {...formMethods}>
<form {...rest} className="space-y-4 rounded-md bg-white px-8 py-10" onSubmit={handleSubmit}> <form {...rest} className="bg-default space-y-4 rounded-md px-8 py-10" onSubmit={handleSubmit}>
<div> <div>
<Button <Button
className="w-full justify-center text-lg" className="w-full justify-center text-lg"
EndIcon={FiExternalLink} EndIcon={ExternalLink}
href="https://console.cal.com" href="https://console.cal.com"
target="_blank"> target="_blank">
{t("purchase_license")} {t("purchase_license")}
</Button> </Button>
<div className="relative flex justify-center"> <div className="relative flex justify-center">
<hr className="my-8 w-full border-[1.5px] border-gray-200" /> <hr className="border-subtle my-8 w-full border-[1.5px]" />
<span className="absolute mt-[22px] bg-white px-3.5 text-sm">OR</span> <span className="bg-default absolute mt-[22px] px-3.5 text-sm">OR</span>
</div> </div>
{t("already_have_key")} {t("already_have_key")}
<Controller <Controller
@@ -108,7 +108,7 @@ const EnterpriseLicense = (
<TextField <TextField
{...formMethods.register("licenseKey")} {...formMethods.register("licenseKey")}
className={classNames( className={classNames(
"mb-0 group-hover:border-gray-400", "group-hover:border-emphasis mb-0",
(checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0" (checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0"
)} )}
placeholder="xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx" placeholder="xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
@@ -116,14 +116,14 @@ const EnterpriseLicense = (
value={value} value={value}
addOnFilled={false} addOnFilled={false}
addOnClassname={classNames( addOnClassname={classNames(
"hover:border-gray-300", "hover:border-default",
errors.licenseKey === undefined && isDirty && "group-hover:border-gray-400" errors.licenseKey === undefined && isDirty && "group-hover:border-emphasis"
)} )}
addOnSuffix={ addOnSuffix={
checkLicenseLoading ? ( checkLicenseLoading ? (
<FiLoader className="h-5 w-5 animate-spin" /> <Loader className="h-5 w-5 animate-spin" />
) : errors.licenseKey === undefined && isDirty ? ( ) : errors.licenseKey === undefined && isDirty ? (
<FiCheck className="h-5 w-5 text-green-700" /> <Check className="h-5 w-5 text-green-700" />
) : undefined ) : undefined
} }
color={errors.licenseKey ? "warn" : ""} color={errors.licenseKey ? "warn" : ""}
+3 -3
View File
@@ -2,7 +2,7 @@ import { useRouter } from "next/router";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { FiCheck } from "@calcom/ui/components/icon"; import { Check } from "@calcom/ui/components/icon";
const StepDone = (props: { const StepDone = (props: {
currentStep: number; currentStep: number;
@@ -23,8 +23,8 @@ const StepDone = (props: {
router.replace(props.nextStepPath); router.replace(props.nextStepPath);
}}> }}>
<div className="min-h-36 my-6 flex flex-col items-center justify-center"> <div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white"> <div className="dark:bg-default flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600">
<FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" /> <Check className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10" />
</div> </div>
<div className="max-w-[420px] text-center"> <div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2> <h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
+6 -5
View File
@@ -2,6 +2,7 @@ import Link from "next/link";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams"; import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { Avatar } from "@calcom/ui"; import { Avatar } from "@calcom/ui";
@@ -18,20 +19,20 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
return ( return (
<Link key={member.id} href={`/${member.username}`}> <Link key={member.id} href={`/${member.username}`}>
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 "> <div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
<Avatar <Avatar
size="md" size="md"
alt={member.name || ""} alt={member.name || ""}
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"} imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
/> />
<section className="line-clamp-4 mt-2 w-full space-y-1"> <section className="line-clamp-4 mt-2 w-full space-y-1">
<p className="font-medium text-gray-900 dark:text-white">{member.name}</p> <p className="text-default font-medium">{member.name}</p>
<div className="line-clamp-3 overflow-ellipsis text-sm font-normal text-gray-500 dark:text-white"> <div className="line-clamp-3 text-subtle overflow-ellipsis text-sm font-normal">
{!isBioEmpty ? ( {!isBioEmpty ? (
<> <>
<div <div
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600" className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: member.safeBio || "" }} dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
/> />
</> </>
) : ( ) : (
+4 -4
View File
@@ -24,18 +24,18 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
)} )}
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}> <div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
{props.heading && <h2 className="font-cal text-center text-3xl text-gray-900">{props.heading}</h2>} {props.heading && <h2 className="font-cal text-emphasis text-center text-3xl">{props.heading}</h2>}
</div> </div>
{props.loading && ( {props.loading && (
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50"> <div className="bg-muted absolute z-50 flex h-screen w-full items-center">
<Loader /> <Loader />
</div> </div>
)} )}
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md"> <div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="mx-2 rounded-md border border-gray-200 bg-white px-4 py-10 sm:px-10"> <div className="bg-default border-subtle mx-2 rounded-md border px-4 py-10 sm:px-10">
{props.children} {props.children}
</div> </div>
<div className="mt-8 text-center text-sm text-gray-600">{props.footerText}</div> <div className="text-default mt-8 text-center text-sm">{props.footerText}</div>
</div> </div>
</div> </div>
); );
+3 -3
View File
@@ -2,7 +2,7 @@ import classNames from "classnames";
import { useState } from "react"; import { useState } from "react";
import type { ControllerRenderProps } from "react-hook-form"; import type { ControllerRenderProps } from "react-hook-form";
import { FiEdit2 } from "@calcom/ui/components/icon"; import { Edit2 } from "@calcom/ui/components/icon";
const EditableHeading = function EditableHeading({ const EditableHeading = function EditableHeading({
value, value,
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
<label className="min-w-8 relative inline-block"> <label className="min-w-8 relative inline-block">
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value}&nbsp;</span> <span className="whitespace-pre text-xl tracking-normal text-transparent">{value}&nbsp;</span>
{!isEditing && isReady && ( {!isEditing && isReady && (
<FiEdit2 className=" ml-1 -mt-px inline h-3 w-3 text-gray-500 group-hover:text-gray-500" /> <Edit2 className=" text-subtle group-hover:text-subtle ml-1 -mt-px inline h-3 w-3" />
)} )}
<input <input
{...passThroughProps} {...passThroughProps}
@@ -29,7 +29,7 @@ const EditableHeading = function EditableHeading({
value={value} value={value}
required required
className={classNames( className={classNames(
"absolute top-0 left-0 w-full cursor-pointer border-none bg-transparent p-0 align-top text-xl text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0" "text-emphasis hover:text-default focus:text-emphasis absolute top-0 left-0 w-full cursor-pointer border-none bg-transparent p-0 align-top text-xl focus:outline-none focus:ring-0"
)} )}
onFocus={(e) => { onFocus={(e) => {
setIsEditing(true); setIsEditing(true);
+2 -2
View File
@@ -1,12 +1,12 @@
import { Tooltip } from "@calcom/ui"; import { Tooltip } from "@calcom/ui";
import { FiInfo } from "@calcom/ui/components/icon"; import { Info } from "@calcom/ui/components/icon";
export default function InfoBadge({ content }: { content: string }) { export default function InfoBadge({ content }: { content: string }) {
return ( return (
<> <>
<Tooltip side="top" content={content}> <Tooltip side="top" content={content}>
<span title={content}> <span title={content}>
<FiInfo className="relative top-px left-1 right-1 mt-px h-4 w-4 text-gray-500" /> <Info className="text-subtle relative top-px left-1 right-1 mt-px h-4 w-4" />
</span> </span>
</Tooltip> </Tooltip>
</> </>
+2 -2
View File
@@ -12,8 +12,8 @@ export default function LinkIconButton(props: LinkIconButtonProps) {
<button <button
type="button" type="button"
{...props} {...props}
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900"> className="text-md hover:bg-emphasis hover:text-emphasis text-default flex items-center rounded-sm px-2 py-1 text-sm font-medium">
<props.Icon className="h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" /> <props.Icon className="text-subtle h-4 w-4 ltr:mr-2 rtl:ml-2" />
{props.children} {props.children}
</button> </button>
</div> </div>
+1 -1
View File
@@ -19,7 +19,7 @@ export default function ModalContainer(
<DialogContent> <DialogContent>
<div <div
className={classNames( className={classNames(
"inline-block w-full transform bg-white text-left align-bottom transition-all sm:align-middle", "bg-default inline-block w-full transform text-left align-bottom transition-all sm:align-middle",
{ {
"sm:w-full sm:max-w-lg ": !props.wide, "sm:w-full sm:max-w-lg ": !props.wide,
"sm:w-4xl sm:max-w-4xl": props.wide, "sm:w-4xl sm:max-w-4xl": props.wide,
+1 -4
View File
@@ -9,10 +9,7 @@ const PoweredByCal = () => {
const isEmbed = useIsEmbed(); const isEmbed = useIsEmbed();
return ( return (
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}> <div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
<Link <Link href={POWERED_BY_URL} target="_blank" className="text-subtle opacity-50 hover:opacity-100">
href={POWERED_BY_URL}
target="_blank"
className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
{t("powered_by")}{" "} {t("powered_by")}{" "}
<img <img
className="relative -mt-px inline h-[10px] w-auto dark:hidden" className="relative -mt-px inline h-[10px] w-auto dark:hidden"
@@ -13,8 +13,8 @@ export default function SettingInputContainer({
<div className="space-y-3"> <div className="space-y-3">
<div className="block sm:flex"> <div className="block sm:flex">
<div className="min-w-48 mb-4 sm:mb-0"> <div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor={htmlFor} className="mt-1 flex text-sm font-medium text-gray-700"> <label htmlFor={htmlFor} className="text-default mt-1 flex text-sm font-medium">
<Icon className="mt-0.5 h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" /> <Icon className="text-subtle mt-0.5 h-4 w-4 ltr:mr-2 rtl:ml-2" />
{label} {label}
</label> </label>
</div> </div>
@@ -14,8 +14,8 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import type { RouterOutputs } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app"; import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Input, Label } from "@calcom/ui"; import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
import { FiCheck, FiEdit2, FiExternalLink } from "@calcom/ui/components/icon"; import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
export enum UsernameChangeStatusEnum { export enum UsernameChangeStatusEnum {
UPGRADE = "UPGRADE", UPGRADE = "UPGRADE",
@@ -195,7 +195,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<span <span
className={classNames( className={classNames(
isInputUsernamePremium ? "border border-orange-400 " : "", isInputUsernamePremium ? "border border-orange-400 " : "",
"hidden h-9 items-center rounded-l-md border border-r-0 border-gray-300 border-r-gray-300 bg-gray-50 px-3 text-sm text-gray-500 md:inline-flex" "border-default bg-muted text-subtle hidden h-9 items-center rounded-l-md border border-r-0 border-r-gray-300 px-3 text-sm md:inline-flex"
)}> )}>
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/ {process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
</span> </span>
@@ -216,7 +216,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
markAsError markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none" ? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
: "border-l-gray-300", : "border-l-gray-300",
disabled ? "bg-gray-100 text-gray-400 focus:border-0" : "" disabled ? "bg-subtle text-muted focus:border-0" : ""
)} )}
value={inputUsernameValue} value={inputUsernameValue}
onChange={(event) => { onChange={(event) => {
@@ -235,7 +235,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
usernameIsAvailable ? "" : "" usernameIsAvailable ? "" : ""
)}> )}>
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>} {isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
{!isInputUsernamePremium && usernameIsAvailable ? <FiCheck className="mt-2 w-6" /> : <></>} {!isInputUsernamePremium && usernameIsAvailable ? <Check className="mt-2 w-6" /> : <></>}
</span> </span>
</div> </div>
</div> </div>
@@ -250,29 +250,30 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>} {markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
<Dialog open={openDialogSaveUsername}> <Dialog open={openDialogSaveUsername}>
<DialogContent> <DialogContent
<div className="flex flex-row"> Icon={Edit2}
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]"> title={t("confirm_username_change_dialog_title")}
<FiEdit2 className="m-auto h-6 w-6" /> description={
</div> <>
<div className="mb-4 w-full px-4 pt-1">
<DialogHeader title={t("confirm_username_change_dialog_title")} />
{usernameChangeCondition && usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && ( {usernameChangeCondition && usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
<p className="mb-4 text-sm text-gray-800">{t("change_username_standard_to_premium")}</p> <p className="text-default mb-4 text-sm">{t("change_username_standard_to_premium")}</p>
)} )}
</>
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm"> }>
<div className="flex flex-row">
<div className="mb-4 w-full px-4 pt-1">
<div className="bg-subtle flex w-full flex-wrap rounded-sm py-3 text-sm">
<div className="flex-1 px-2"> <div className="flex-1 px-2">
<p className="text-gray-500">{t("current_username")}</p> <p className="text-subtle">{t("current_username")}</p>
<p className="mt-1" data-testid="current-username"> <p className="text-emphasis mt-1" data-testid="current-username">
{currentUsername} {currentUsername}
</p> </p>
</div> </div>
<div className="ml-6 flex-1"> <div className="ml-6 flex-1">
<p className="text-gray-500" data-testid="new-username"> <p className="text-subtle" data-testid="new-username">
{t("new_username")} {t("new_username")}
</p> </p>
<p>{inputUsernameValue}</p> <p className="text-emphasis">{inputUsernameValue}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -287,7 +288,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
data-testid="go-to-billing" data-testid="go-to-billing"
href={paymentLink}> href={paymentLink}>
<> <>
{t("go_to_stripe_billing")} <FiExternalLink className="ml-1 h-4 w-4" /> {t("go_to_stripe_billing")} <ExternalLink className="ml-1 h-4 w-4" />
</> </>
</Button> </Button>
)} )}
@@ -8,8 +8,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { TRPCClientErrorLike } from "@calcom/trpc/client"; import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app"; import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, TextField } from "@calcom/ui"; import { Button, Dialog, DialogClose, DialogContent, TextField } from "@calcom/ui";
import { FiCheck, FiEdit2 } from "@calcom/ui/components/icon"; import { Check, Edit2 } from "@calcom/ui/components/icon";
interface ICustomUsernameProps { interface ICustomUsernameProps {
currentUsername: string | undefined; currentUsername: string | undefined;
@@ -79,7 +79,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
const ActionButtons = () => { const ActionButtons = () => {
return usernameIsAvailable && currentUsername !== inputUsernameValue ? ( return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
<div className="rlt:space-x-reverse mt-px flex flex-row space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse"> <div className="ms-2 me-2 mt-px flex flex-row space-x-2">
<Button <Button
type="button" type="button"
onClick={() => setOpenDialogSaveUsername(true)} onClick={() => setOpenDialogSaveUsername(true)}
@@ -123,7 +123,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
autoCapitalize="none" autoCapitalize="none"
autoCorrect="none" autoCorrect="none"
className={classNames( className={classNames(
"mb-0 mt-0 rounded-md ltr:rounded-l-none rtl:rounded-r-none", "mb-0 mt-0 rounded-md rounded-l-none",
markAsError markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0" ? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
: "" : ""
@@ -137,7 +137,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
{currentUsername !== inputUsernameValue && ( {currentUsername !== inputUsernameValue && (
<div className="absolute right-[2px] top-6 flex flex-row"> <div className="absolute right-[2px] top-6 flex flex-row">
<span className={classNames("mx-2 py-2")}> <span className={classNames("mx-2 py-2")}>
{usernameIsAvailable ? <FiCheck className="w-6" /> : <></>} {usernameIsAvailable ? <Check className="w-6" /> : <></>}
</span> </span>
</div> </div>
)} )}
@@ -154,31 +154,25 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
</div> </div>
)} )}
<Dialog open={openDialogSaveUsername}> <Dialog open={openDialogSaveUsername}>
<DialogContent> <DialogContent type="confirmation" Icon={Edit2} title={t("confirm_username_change_dialog_title")}>
<div style={{ display: "flex", flexDirection: "row" }}> <div className="flex flex-row">
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]"> <div className="mb-4 w-full pt-1">
<FiEdit2 className="m-auto h-6 w-6" /> <div className="bg-subtle flex w-full flex-wrap rounded-sm py-3 text-sm">
</div>
<div className="mb-4 w-full px-4 pt-1">
<DialogHeader title={t("confirm_username_change_dialog_title")} />
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
<div className="flex-1 px-2"> <div className="flex-1 px-2">
<p className="text-gray-500">{t("current_username")}</p> <p className="text-subtle">{t("current_username")}</p>
<p className="mt-1" data-testid="current-username"> <p className="text-emphasis mt-1" data-testid="current-username">
{currentUsername} {currentUsername}
</p> </p>
</div> </div>
<div className="flex-1"> <div className="ml-6 flex-1">
<p className="text-gray-500" data-testid="new-username"> <p className="text-subtle" data-testid="new-username">
{t("new_username")} {t("new_username")}
</p> </p>
<p>{inputUsernameValue}</p> <p className="text-emphasis">{inputUsernameValue}</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="mt-4 flex flex-row-reverse gap-x-2"> <div className="mt-4 flex flex-row-reverse gap-x-2">
<Button <Button
type="button" type="button"
@@ -22,7 +22,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
{React.createElement( {React.createElement(
descriptionAsLabel ? "div" : "label", descriptionAsLabel ? "div" : "label",
{ {
className: "flex text-sm font-medium text-gray-700", className: "flex text-sm font-medium text-default",
...(!descriptionAsLabel ...(!descriptionAsLabel
? { ? {
htmlFor: rest.id, htmlFor: rest.id,
@@ -40,7 +40,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
{ {
className: classNames( className: classNames(
"relative flex items-start", "relative flex items-start",
descriptionAsLabel ? "text-gray-700" : "text-gray-900" descriptionAsLabel ? "text-default" : "text-emphasis"
), ),
}, },
<> <>
@@ -49,10 +49,10 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
{...rest} {...rest}
ref={ref} ref={ref}
type="checkbox" type="checkbox"
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300" className="text-primary-600 focus:ring-primary-500 border-default h-4 w-4 rounded"
/> />
</div> </div>
<span className="text-sm ltr:ml-3 rtl:mr-3">{description}</span> <span className="ms-3 text-sm">{description}</span>
</> </>
)} )}
{informationIconText && <InfoBadge content={informationIconText} />} {informationIconText && <InfoBadge content={informationIconText} />}
@@ -3,7 +3,7 @@ import type { Props } from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Avatar } from "@calcom/ui"; import { Avatar } from "@calcom/ui";
import { FiX } from "@calcom/ui/components/icon"; import { X } from "@calcom/ui/components/icon";
import Select from "@components/ui/form/Select"; import Select from "@components/ui/form/Select";
@@ -49,9 +49,9 @@ export const CheckedSelect = ({
alt={option.label} alt={option.label}
/> />
{option.label} {option.label}
<FiX <X
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))} onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
className="float-right mt-0.5 h-5 w-5 cursor-pointer text-gray-500" className="text-subtle float-right mt-0.5 h-5 w-5 cursor-pointer"
/> />
</div> </div>
))} ))}
+3 -3
View File
@@ -3,7 +3,7 @@ import "react-calendar/dist/Calendar.css";
import "react-date-picker/dist/DatePicker.css"; import "react-date-picker/dist/DatePicker.css";
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle"; import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
import { FiCalendar } from "@calcom/ui/components/icon"; import { Calendar } from "@calcom/ui/components/icon";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
@@ -19,11 +19,11 @@ export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }
return ( return (
<PrimitiveDatePicker <PrimitiveDatePicker
className={classNames( className={classNames(
"focus:ring-primary-500 focus:border-primary-500 rounded-sm border border-gray-300 p-1 pl-2 text-sm", "focus:ring-primary-500 focus:border-primary-500 border-default rounded-sm border p-1 pl-2 text-sm",
className className
)} )}
clearIcon={null} clearIcon={null}
calendarIcon={<FiCalendar className="h-5 w-5 text-gray-500" />} calendarIcon={<Calendar className="text-subtle h-5 w-5" />}
value={date} value={date}
minDate={minDate} minDate={minDate}
disabled={disabled} disabled={disabled}
@@ -19,7 +19,10 @@ export type GroupOptionType = GroupBase<LocationOption>;
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => { const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
return ( return (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5" />} {/* TODO: figure out a way to invert icons when in dark mode. We can't just
dark:invert due to google meet cal etc all breaking when we do this
*/}
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5 dark:hidden" />}
<span className={classNames("text-sm font-medium")}>{label}</span> <span className={classNames("text-sm font-medium")}>{label}</span>
</div> </div>
); );
@@ -48,7 +51,7 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
<span>{e.label}</span> <span>{e.label}</span>
</div> </div>
)} )}
formatGroupLabel={(e) => <p className="text-xs font-medium text-gray-600">{e.label}</p>} formatGroupLabel={(e) => <p className="text-default text-xs font-medium">{e.label}</p>}
{...props} {...props}
/> />
); );
+3 -3
View File
@@ -11,7 +11,7 @@ const MinutesField = forwardRef<HTMLInputElement, Props>(({ label, ...rest }, re
<div className="block sm:flex"> <div className="block sm:flex">
{!!label && ( {!!label && (
<div className="min-w-48 mb-4 sm:mb-0"> <div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor={rest.id} className="flex h-full items-center text-sm font-medium text-gray-700"> <label htmlFor={rest.id} className="text-default flex h-full items-center text-sm font-medium">
{label} {label}
</label> </label>
</div> </div>
@@ -23,12 +23,12 @@ const MinutesField = forwardRef<HTMLInputElement, Props>(({ label, ...rest }, re
ref={ref} ref={ref}
type="number" type="number"
className={classNames( className={classNames(
"block w-full rounded-sm border-gray-300 pl-2 pr-12 text-sm", "border-default block w-full rounded-sm pl-2 pr-12 text-sm",
rest.className rest.className
)} )}
/> />
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"> <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span className="text-sm text-gray-500" id="duration"> <span className="text-subtle text-sm" id="duration">
mins mins
</span> </span>
</div> </div>
+85 -19
View File
@@ -6,7 +6,8 @@ import { appWithTranslation } from "next-i18next";
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app"; import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
import type { NextRouter } from "next/router"; import type { NextRouter } from "next/router";
import type { ComponentProps, ReactNode } from "react"; import { useRouter } from "next/router";
import type { ComponentProps, PropsWithChildren, ReactNode } from "react";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic"; import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic"; import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
@@ -26,7 +27,8 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & { export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
Component: NextAppProps["Component"] & { Component: NextAppProps["Component"] & {
requiresLicense?: boolean; requiresLicense?: boolean;
isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean); isThemeSupported?: boolean;
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode; getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
}; };
@@ -60,6 +62,83 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
return <I18nextAdapter {...passedProps} />; return <I18nextAdapter {...passedProps} />;
}; };
const enum ThemeSupport {
// e.g. Login Page
None = "none",
// Entire App except Booking Pages
App = "systemOnly",
// Booking Pages(including Routing Forms)
Booking = "userConfigured",
}
const CalcomThemeProvider = (
props: PropsWithChildren<
WithNonceProps & {
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
isThemeSupported?: boolean;
}
>
) => {
// We now support the inverse of how we handled it in the past. Setting this to false will disable theme.
// undefined or true means we use system theme
const router = useRouter();
const isBookingPage = (() => {
if (typeof props.isBookingPage === "function") {
return props.isBookingPage({ router: router });
}
return props.isBookingPage;
})();
const themeSupport = isBookingPage
? ThemeSupport.Booking
: // if isThemeSupported is explicitly false, we don't use theme there
props.isThemeSupported === false
? ThemeSupport.None
: ThemeSupport.App;
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
// One such example is our Embeds Demo and Testing page at http://localhost:3100
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
const isEmbedMode = typeof embedNamespace === "string";
const storageKey = isEmbedMode
? `embed-theme-${embedNamespace}`
: themeSupport === ThemeSupport.App
? "app-theme"
: themeSupport === ThemeSupport.Booking
? "booking-theme"
: undefined;
return (
<ThemeProvider
nonce={props.nonce}
enableColorScheme={false}
enableSystem={themeSupport !== ThemeSupport.None}
forcedTheme={forcedTheme}
storageKey={storageKey}
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
// This is how login to dashboard soft navigation changes theme from light to dark
key={storageKey}
attribute="class">
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
{typeof window !== "undefined" && !isEmbedMode && (
<style jsx global>
{`
.dark {
color-scheme: dark;
}
`}
</style>
)}
{props.children}
</ThemeProvider>
);
};
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) { function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
const flags = useFlags(); const flags = useFlags();
return <FeatureProvider value={flags}>{children}</FeatureProvider>; return <FeatureProvider value={flags}>{children}</FeatureProvider>;
@@ -69,33 +148,20 @@ const AppProviders = (props: AppPropsWithChildren) => {
const session = trpc.viewer.public.session.useQuery().data; const session = trpc.viewer.public.session.useQuery().data;
// No need to have intercom on public pages - Good for Page Performance // No need to have intercom on public pages - Good for Page Performance
const isPublicPage = usePublicPage(); const isPublicPage = usePublicPage();
const isThemeSupported =
typeof props.Component.isThemeSupported === "function"
? props.Component.isThemeSupported({ router: props.router })
: props.Component.isThemeSupported;
const forcedTheme = isThemeSupported ? undefined : "light";
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
// One such example is our Embeds Demo and Testing page at http://localhost:3100
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
const storageKey = typeof embedNamespace === "string" ? `embed-theme-${embedNamespace}` : "theme";
const RemainingProviders = ( const RemainingProviders = (
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}> <EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<SessionProvider session={session || undefined}> <SessionProvider session={session || undefined}>
<CustomI18nextProvider {...props}> <CustomI18nextProvider {...props}>
<TooltipProvider> <TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */} <CalcomThemeProvider
<ThemeProvider
nonce={props.pageProps.nonce} nonce={props.pageProps.nonce}
enableColorScheme={false} isThemeSupported={props.Component.isThemeSupported}
storageKey={storageKey} isBookingPage={props.Component.isBookingPage}>
forcedTheme={forcedTheme}
attribute="class">
<FeatureFlagsProvider> <FeatureFlagsProvider>
<MetaProvider>{props.children}</MetaProvider> <MetaProvider>{props.children}</MetaProvider>
</FeatureFlagsProvider> </FeatureFlagsProvider>
</ThemeProvider> </CalcomThemeProvider>
</TooltipProvider> </TooltipProvider>
</CustomI18nextProvider> </CustomI18nextProvider>
</SessionProvider> </SessionProvider>
+1 -4
View File
@@ -13,9 +13,6 @@ function getCspPolicy(nonce: string) {
// We can remove 'unsafe-inline' from style-src when we add nonces to all style tags // We can remove 'unsafe-inline' from style-src when we add nonces to all style tags
// Maybe see how @next-safe/middleware does it if it's supported. // Maybe see how @next-safe/middleware does it if it's supported.
const useNonStrictPolicy = CSP_POLICY === "non-strict"; const useNonStrictPolicy = CSP_POLICY === "non-strict";
const SENTRY_ENDPOINT = process.env.NEXT_PUBLIC_SENTRY_DSN
? new URL(process.env.NEXT_PUBLIC_SENTRY_DSN, "http://base_url").origin
: "";
// We add WEBAPP_URL to img-src because of booking pages, which end up loading images from app.cal.com on cal.com // We add WEBAPP_URL to img-src because of booking pages, which end up loading images from app.cal.com on cal.com
// FIXME: Write a layer to extract out EventType Analytics tracking endpoints and add them to img-src or connect-src as needed. e.g. fathom, Google Analytics and others // FIXME: Write a layer to extract out EventType Analytics tracking endpoints and add them to img-src or connect-src as needed. e.g. fathom, Google Analytics and others
@@ -36,7 +33,7 @@ function getCspPolicy(nonce: string) {
} app.cal.com; } app.cal.com;
font-src 'self'; font-src 'self';
img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:; img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
connect-src 'self' ${SENTRY_ENDPOINT} connect-src 'self'
`; `;
} }
+3 -21
View File
@@ -1,6 +1,5 @@
require("dotenv").config({ path: "../../.env" }); require("dotenv").config({ path: "../../.env" });
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const { withSentryConfig } = require("@sentry/nextjs");
const os = require("os"); const os = require("os");
const { withAxiom } = require("next-axiom"); const { withAxiom } = require("next-axiom");
@@ -92,11 +91,11 @@ const nextConfig = {
"@calcom/prisma", "@calcom/prisma",
"@calcom/trpc", "@calcom/trpc",
"@calcom/ui", "@calcom/ui",
"lucide-react",
], ],
modularizeImports: { modularizeImports: {
"@calcom/ui/components/icon": { "@calcom/ui/components/icon": {
transform: "@react-icons/all-files/fi/{{member}}", transform: "lucide-react/dist/esm/icons/{{ kebabCase member }}",
skipDefaultConversion: true,
preventFullImport: true, preventFullImport: true,
}, },
"@calcom/features/insights/components": { "@calcom/features/insights/components": {
@@ -318,21 +317,4 @@ const nextConfig = {
}, },
}; };
const sentryWebpackPluginOptions = { module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
silent: true, // Suppresses all logs
};
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
nextConfig.sentry = {
hideSourceMaps: true,
// Prevents Sentry from running on this Edge function, where Sentry doesn't work yet (build whould crash the api route).
excludeServerRoutes: [/\/api\/social\/og\/image\/?/],
};
}
// Sentry should be the last thing to export to catch everything right
module.exports = process.env.NEXT_PUBLIC_SENTRY_DSN
? withSentryConfig(moduleExports, sentryWebpackPluginOptions)
: moduleExports;
+3 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "2.7.6", "version": "2.8.2",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
@@ -57,7 +57,6 @@
"@radix-ui/react-switch": "^1.0.0", "@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-toggle-group": "^1.0.0", "@radix-ui/react-toggle-group": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0",
"@sentry/nextjs": "^7.20.0",
"@stripe/react-stripe-js": "^1.10.0", "@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0", "@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9", "@tanstack/react-query": "^4.3.9",
@@ -65,7 +64,7 @@
"@types/turndown": "^5.0.1", "@types/turndown": "^5.0.1",
"@vercel/edge-config": "^0.1.1", "@vercel/edge-config": "^0.1.1",
"@vercel/edge-functions-ui": "^0.2.1", "@vercel/edge-functions-ui": "^0.2.1",
"@vercel/og": "^0.0.21", "@vercel/og": "^0.5.0",
"accept-language-parser": "^1.5.0", "accept-language-parser": "^1.5.0",
"async": "^3.2.4", "async": "^3.2.4",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
@@ -170,7 +169,7 @@
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"msw": "^0.42.3", "msw": "^0.42.3",
"postcss": "^8.4.18", "postcss": "^8.4.18",
"tailwindcss": "^3.2.1", "tailwindcss": "^3.3.1",
"ts-jest": "^28.0.8", "ts-jest": "^28.0.8",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.9.4" "typescript": "^4.9.4"
+51 -49
View File
@@ -3,10 +3,10 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants"; import { DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui"; import { HeadSeo } from "@calcom/ui";
import { FiBookOpen, FiCheck, FiChevronRight, FiFileText } from "@calcom/ui/components/icon"; import { BookOpen, Check, ChevronRight, FileText } from "@calcom/ui/components/icon";
import { ssgInit } from "@server/lib/ssg"; import { ssgInit } from "@server/lib/ssg";
@@ -20,13 +20,13 @@ export default function Custom404() {
{ {
title: t("documentation"), title: t("documentation"),
description: t("documentation_description"), description: t("documentation_description"),
icon: FiFileText, icon: FileText,
href: DOCS_URL, href: DOCS_URL,
}, },
{ {
title: t("blog"), title: t("blog"),
description: t("blog_description"), description: t("blog_description"),
icon: FiBookOpen, icon: BookOpen,
href: `${WEBSITE_URL}/blog`, href: `${WEBSITE_URL}/blog`,
}, },
]; ];
@@ -88,21 +88,21 @@ export default function Custom404() {
noindex: true, noindex: true,
}} }}
/> />
<div className="min-h-screen bg-white px-4" data-testid="404-page"> <div className="bg-default min-h-screen px-4" data-testid="404-page">
<main className="mx-auto max-w-xl pt-16 pb-6 sm:pt-24"> <main className="mx-auto max-w-xl pt-16 pb-6 sm:pt-24">
{isSignup && process.env.NEXT_PUBLIC_WEBAPP_URL !== "https://app.cal.com" ? ( {isSignup && process.env.NEXT_PUBLIC_WEBAPP_URL !== "https://app.cal.com" ? (
<div> <div>
<div> <div>
<p className="text-sm font-semibold uppercase tracking-wide text-black"> <p className="text-emphasis text-sm font-semibold uppercase tracking-wide">
{t("missing_license")} {t("missing_license")}
</p> </p>
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900"> <h1 className="font-cal text-emphasis mt-2 text-3xl font-extrabold">
{t("signup_requires")} {t("signup_requires")}
</h1> </h1>
<p className="mt-4">{t("signup_requires_description", { companyName: COMPANY_NAME })}</p> <p className="mt-4">{t("signup_requires_description", { companyName: "Cal.com" })}</p>
</div> </div>
<div className="mt-12"> <div className="mt-12">
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500"> <h2 className="text-subtle text-sm font-semibold uppercase tracking-wide">
{t("next_steps")} {t("next_steps")}
</h2> </h2>
<ul role="list" className="mt-4"> <ul role="list" className="mt-4">
@@ -112,48 +112,48 @@ export default function Custom404() {
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50"> <span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
<FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" /> <Check className="h-6 w-6 text-green-500" aria-hidden="true" />
</span> </span>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="text-base font-medium text-gray-900"> <h3 className="text-emphasis text-base font-medium">
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2"> <span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="focus:outline-none"> <span className="focus:outline-none">
<span className="absolute inset-0" aria-hidden="true" /> <span className="absolute inset-0" aria-hidden="true" />
{t("acquire_commercial_license")} {t("acquire_commercial_license")}
</span> </span>
</span> </span>
</h3> </h3>
<p className="text-base text-gray-500">{t("the_infrastructure_plan")}</p> <p className="text-subtle text-base">{t("the_infrastructure_plan")}</p>
</div> </div>
<div className="flex-shrink-0 self-center"> <div className="flex-shrink-0 self-center">
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" /> <ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div> </div>
</a> </a>
</li> </li>
</ul> </ul>
<ul role="list" className="divide-y divide-gray-200 border-gray-200"> <ul role="list" className="border-subtle divide-subtle divide-y">
<li className="px-4 py-2"> <li className="px-4 py-2">
<Link <Link
href={`${DEVELOPER_DOCS}/self-hosting/installation`} href="https://cal.com/self-hosting/installation"
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50"> <span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" /> <FileText className="text-default h-6 w-6" aria-hidden="true" />
</span> </span>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="text-base font-medium text-gray-900"> <h3 className="text-emphasis text-base font-medium">
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2"> <span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" /> <span className="absolute inset-0" aria-hidden="true" />
{t("prisma_studio_tip")} {t("prisma_studio_tip")}
</span> </span>
</h3> </h3>
<p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p> <p className="text-subtle text-base">{t("prisma_studio_tip_description")}</p>
</div> </div>
<div className="flex-shrink-0 self-center"> <div className="flex-shrink-0 self-center">
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" /> <ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div> </div>
</Link> </Link>
</li> </li>
@@ -162,7 +162,7 @@ export default function Custom404() {
href={JOIN_SLACK} href={JOIN_SLACK}
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50"> <span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<svg <svg
viewBox="0 0 2447.6 2452.5" viewBox="0 0 2447.6 2452.5"
className="h-6 w-6" className="h-6 w-6"
@@ -189,16 +189,16 @@ export default function Custom404() {
</span> </span>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="text-base font-medium text-gray-900"> <h3 className="text-emphasis text-base font-medium">
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2"> <span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" /> <span className="absolute inset-0" aria-hidden="true" />
Slack Slack
</span> </span>
</h3> </h3>
<p className="text-base text-gray-500">{t("join_our_community")}</p> <p className="text-subtle text-base">{t("join_our_community")}</p>
</div> </div>
<div className="flex-shrink-0 self-center"> <div className="flex-shrink-0 self-center">
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" /> <ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div> </div>
</a> </a>
</li> </li>
@@ -206,7 +206,7 @@ export default function Custom404() {
<div className="mt-8"> <div className="mt-8">
<Link <Link
href={`${WEBSITE_URL}/enterprise`} href={`${WEBSITE_URL}/enterprise`}
className="text-base font-medium text-black hover:text-gray-500"> className="hover:text-subtle text-emphasis text-base font-medium">
{t("contact_sales")} {t("contact_sales")}
<span aria-hidden="true"> &rarr;</span> <span aria-hidden="true"> &rarr;</span>
</Link> </Link>
@@ -216,8 +216,10 @@ export default function Custom404() {
) : ( ) : (
<> <>
<div className="text-center"> <div className="text-center">
<p className="text-sm font-semibold uppercase tracking-wide text-black">{t("error_404")}</p> <p className="text-emphasis text-sm font-semibold uppercase tracking-wide">
<h1 className="font-cal mt-2 text-4xl font-extrabold text-gray-900 sm:text-5xl"> {t("error_404")}
</p>
<h1 className="font-cal text-emphasis mt-2 text-4xl font-extrabold sm:text-5xl">
{isSuccessPage ? "Booking not found" : t("page_doesnt_exist")} {isSuccessPage ? "Booking not found" : t("page_doesnt_exist")}
</h1> </h1>
{isSubpage ? ( {isSubpage ? (
@@ -242,7 +244,7 @@ export default function Custom404() {
)} )}
</div> </div>
<div className="mt-12"> <div className="mt-12">
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500"> <h2 className="text-subtle text-sm font-semibold uppercase tracking-wide">
{t("popular_pages")} {t("popular_pages")}
</h2> </h2>
{!isSubpage && isCalcom && ( {!isSubpage && isCalcom && (
@@ -255,50 +257,50 @@ export default function Custom404() {
rel="noreferrer"> rel="noreferrer">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50"> <span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
<FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" /> <Check className="h-6 w-6 text-green-500" aria-hidden="true" />
</span> </span>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="text-base font-medium text-gray-900"> <h3 className="text-emphasis text-base font-medium">
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2"> <span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="focus:outline-none"> <span className="focus:outline-none">
<span className="absolute inset-0" aria-hidden="true" /> <span className="absolute inset-0" aria-hidden="true" />
{t("register")} <strong className="text-green-500">{username}</strong> {t("register")} <strong className="text-green-500">{username}</strong>
</span> </span>
</span> </span>
</h3> </h3>
<p className="text-base text-gray-500">{t("claim_username_and_schedule_events")}</p> <p className="text-subtle text-base">{t("claim_username_and_schedule_events")}</p>
</div> </div>
<div className="flex-shrink-0 self-center"> <div className="flex-shrink-0 self-center">
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" /> <ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div> </div>
</a> </a>
</li> </li>
</ul> </ul>
)} )}
<ul role="list" className="mt-4 divide-y divide-gray-200 border-gray-200"> <ul role="list" className="border-subtle divide-subtle mt-4 divide-y">
{links.map((link, linkIdx) => ( {links.map((link, linkIdx) => (
<li key={linkIdx} className="px-4 py-2"> <li key={linkIdx} className="px-4 py-2">
<Link <Link
href={link.href} href={link.href}
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50"> <span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<link.icon className="h-6 w-6 text-gray-700" aria-hidden="true" /> <link.icon className="text-default h-6 w-6" aria-hidden="true" />
</span> </span>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="text-base font-medium text-gray-900"> <h3 className="text-emphasis text-base font-medium">
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2"> <span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" /> <span className="absolute inset-0" aria-hidden="true" />
{link.title} {link.title}
</span> </span>
</h3> </h3>
<p className="text-base text-gray-500">{link.description}</p> <p className="text-subtle text-base">{link.description}</p>
</div> </div>
<div className="flex-shrink-0 self-center"> <div className="flex-shrink-0 self-center">
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" /> <ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div> </div>
</Link> </Link>
</li> </li>
@@ -308,7 +310,7 @@ export default function Custom404() {
href={JOIN_SLACK} href={JOIN_SLACK}
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50"> <span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<svg <svg
viewBox="0 0 2447.6 2452.5" viewBox="0 0 2447.6 2452.5"
className="h-6 w-6" className="h-6 w-6"
@@ -335,22 +337,22 @@ export default function Custom404() {
</span> </span>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="text-base font-medium text-gray-900"> <h3 className="text-emphasis text-base font-medium">
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2"> <span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" /> <span className="absolute inset-0" aria-hidden="true" />
Slack Slack
</span> </span>
</h3> </h3>
<p className="text-base text-gray-500">{t("join_our_community")}</p> <p className="text-subtle text-base">{t("join_our_community")}</p>
</div> </div>
<div className="flex-shrink-0 self-center"> <div className="flex-shrink-0 self-center">
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" /> <ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div> </div>
</a> </a>
</li> </li>
</ul> </ul>
<div className="mt-8"> <div className="mt-8">
<Link href="/" className="text-base font-medium text-black hover:text-gray-500"> <Link href="/" className="hover:text-subtle text-emphasis text-base font-medium">
{t("or_go_back_home")} {t("or_go_back_home")}
<span aria-hidden="true"> &rarr;</span> <span aria-hidden="true"> &rarr;</span>
</Link> </Link>
+9 -9
View File
@@ -4,37 +4,37 @@ import { useRouter } from "next/router";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants"; import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast } from "@calcom/ui"; import { Button, showToast } from "@calcom/ui";
import { FiCopy } from "@calcom/ui/components/icon"; import { Copy } from "@calcom/ui/components/icon";
export default function Error500() { export default function Error500() {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
return ( return (
<div className="flex h-screen bg-gray-100"> <div className="bg-subtle flex h-screen">
<Head> <Head>
<title>Something unexpected occurred | {APP_NAME}</title> <title>Something unexpected occurred | {APP_NAME}</title>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<div className="rtl: m-auto rounded-md bg-white p-10 text-right ltr:text-left"> <div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
<h1 className="font-cal text-6xl text-black">500</h1> <h1 className="font-cal text-emphasis text-6xl">500</h1>
<h2 className="mt-6 text-2xl font-medium text-black">It&apos;s not you, it&apos;s us.</h2> <h2 className="text-emphasis mt-6 text-2xl font-medium">It&apos;s not you, it&apos;s us.</h2>
<p className="mt-4 mb-6 max-w-2xl text-sm text-gray-600"> <p className="text-default mt-4 mb-6 max-w-2xl text-sm">
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
away for you. away for you.
</p> </p>
{router.query.error && ( {router.query.error && (
<div className="mb-8 flex flex-col"> <div className="mb-8 flex flex-col">
<p className="mb-4 max-w-2xl text-sm text-gray-600"> <p className="text-default mb-4 max-w-2xl text-sm">
Please provide the following text when contacting support to better help you: Please provide the following text when contacting support to better help you:
</p> </p>
<pre className="w-full max-w-2xl whitespace-normal break-words rounded-md bg-gray-200 p-4 text-gray-900"> <pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
{router.query.error} {router.query.error}
<br /> <br />
<Button <Button
color="secondary" color="secondary"
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300" className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
StartIcon={FiCopy} StartIcon={Copy}
onClick={() => { onClick={() => {
navigator.clipboard.writeText(router.query.error as string); navigator.clipboard.writeText(router.query.error as string);
showToast("Link copied!", "success"); showToast("Link copied!", "success");
+28 -44
View File
@@ -3,7 +3,6 @@ import classNames from "classnames";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { import {
@@ -14,7 +13,6 @@ import {
} from "@calcom/embed-core/embed-iframe"; } from "@calcom/embed-core/embed-iframe";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components"; import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage"; import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import CustomBranding from "@calcom/lib/CustomBranding";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import defaultEvents, { import defaultEvents, {
getDynamicEventDescription, getDynamicEventDescription,
@@ -25,12 +23,11 @@ import defaultEvents, {
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects"; import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui"; import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
import { FiArrowRight } from "@calcom/ui/components/icon"; import { ArrowRight } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr"; import type { EmbedProps } from "@lib/withEmbedSsr";
@@ -48,9 +45,9 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
const groupEventTypes = props.users.some((user) => !user.allowDynamicBooking) ? ( const groupEventTypes = props.users.some((user) => !user.allowDynamicBooking) ? (
<div className="space-y-6" data-testid="event-types"> <div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900"> <div className="overflow-hidden rounded-sm border ">
<div className="p-8 text-center text-gray-400 dark:text-white"> <div className="text-muted p-8 text-center">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">{" " + t("unavailable")}</h2> <h2 className="font-cal text-default mb-2 text-3xl">{" " + t("unavailable")}</h2>
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled") as string}</p> <p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled") as string}</p>
</div> </div>
</div> </div>
@@ -60,14 +57,14 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
{eventTypes.map((type, index) => ( {eventTypes.map((type, index) => (
<li <li
key={index} key={index}
className="dark:bg-darkgray-100 dark:border-darkgray-300 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50"> className=" border-subtle bg-default hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
<FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" /> <ArrowRight className="text-emphasis absolute right-3 top-3 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
<Link <Link
href={getUsernameSlugLink({ users: props.users, slug: type.slug })} href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
className="flex justify-between px-6 py-4" className="flex justify-between px-6 py-4"
data-testid="event-type-link"> data-testid="event-type-link">
<div className="flex-shrink"> <div className="flex-shrink">
<p className="dark:text-darkgray-700 text-sm font-semibold text-gray-900">{type.title}</p> <p className=" text-emphasis text-sm font-semibold">{type.title}</p>
<EventTypeDescription className="text-sm" eventType={type} /> <EventTypeDescription className="text-sm" eventType={type} />
</div> </div>
<div className="mt-1 self-center"> <div className="mt-1 self-center">
@@ -94,14 +91,15 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
const query = { ...router.query }; const query = { ...router.query };
delete query.user; // So it doesn't display in the Link (and make tests fail) delete query.user; // So it doesn't display in the Link (and make tests fail)
const nameOrUsername = user.name || user.username || ""; const nameOrUsername = user.name || user.username || "";
const telemetry = useTelemetry();
useEffect(() => { /*
const telemetry = useTelemetry();
useEffect(() => {
if (top !== window) { if (top !== window) {
//page_view will be collected automatically by _middleware.ts //page_view will be collected automatically by _middleware.ts
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]")); telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
} }
}, [telemetry, router.asPath]); }, [telemetry, router.asPath]); */
const isEventListEmpty = eventTypes.length === 0; const isEventListEmpty = eventTypes.length === 0;
return ( return (
<> <>
@@ -118,53 +116,39 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
: [{ username: `${user.username}`, name: `${user.name}` }], : [{ username: `${user.username}`, name: `${user.name}` }],
}} }}
/> />
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<div <div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "max-w-3xl" : "",
"dark:bg-darkgray-50"
)}>
<main <main
className={classNames( className={classNames(
shouldAlignCentrally ? "mx-auto" : "", shouldAlignCentrally ? "mx-auto" : "",
isEmbed isEmbed ? " border-booker border-booker-width bg-default rounded-md border" : "",
? " border-bookinglightest dark:bg-darkgray-50 rounded-md border bg-white sm:dark:border-gray-600"
: "",
"max-w-3xl py-24 px-4" "max-w-3xl py-24 px-4"
)}> )}>
{isSingleUser && ( // When we deal with a single user, not dynamic group {isSingleUser && ( // When we deal with a single user, not dynamic group
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<Avatar imageSrc={user.avatar} size="xl" alt={nameOrUsername} /> <Avatar imageSrc={user.avatar} size="xl" alt={nameOrUsername} />
<h1 className="font-cal mb-1 text-3xl text-gray-900 dark:text-white"> <h1 className="font-cal text-emphasis mb-1 text-3xl">
{nameOrUsername} {nameOrUsername}
{user.verified && ( {user.verified && <BadgeCheckIcon className=" mx-1 -mt-1 inline h-6 w-6 text-blue-500" />}
<BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" />
)}
</h1> </h1>
{!isBioEmpty && ( {!isBioEmpty && (
<> <>
<div <div
className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600" className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: props.safeBio }} dangerouslySetInnerHTML={{ __html: props.safeBio }}
/> />
</> </>
)} )}
</div> </div>
)} )}
<div <div
className={classNames( className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
"rounded-md ",
!isEventListEmpty && "dark:border-darkgray-300 border border-gray-200"
)}
data-testid="event-types"> data-testid="event-types">
{user.away ? ( {user.away ? (
<div className="overflow-hidden rounded-sm border dark:border-gray-900"> <div className="overflow-hidden rounded-sm border ">
<div className="p-8 text-center text-gray-400 dark:text-white"> <div className="text-muted p-8 text-center">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white"> <h2 className="font-cal text-default mb-2 text-3xl">😴{" " + t("user_away")}</h2>
😴{" " + t("user_away")}
</h2>
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p> <p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
</div> </div>
</div> </div>
@@ -175,8 +159,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<div <div
key={type.id} key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }} style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
className="dark:bg-darkgray-100 dark:hover:bg-darkgray-200 dark:border-darkgray-300 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50"> className=" border-subtle bg-default hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
<FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" /> <ArrowRight className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */} {/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<div className="block w-full p-5"> <div className="block w-full p-5">
<Link <Link
@@ -193,17 +177,16 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
}} }}
data-testid="event-type-link"> data-testid="event-type-link">
<div className="flex flex-wrap items-center"> <div className="flex flex-wrap items-center">
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700"> <h2 className=" text-default pr-2 text-sm font-semibold">{type.title}</h2>
{type.title}
</h2>
</div> </div>
<EventTypeDescription eventType={type} /> <EventTypeDescription eventType={type} isPublic={true} />
</Link> </Link>
</div> </div>
</div> </div>
)) ))
)} )}
</div> </div>
{isEventListEmpty && <EmptyPage name={user.name ?? "User"} />} {isEventListEmpty && <EmptyPage name={user.name ?? "User"} />}
</main> </main>
<Toaster position="bottom-right" /> <Toaster position="bottom-right" />
@@ -211,7 +194,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
</> </>
); );
} }
User.isThemeSupported = true;
User.isBookingPage = true;
const getEventTypesWithHiddenFromDB = async (userId: number) => { const getEventTypesWithHiddenFromDB = async (userId: number) => {
return ( return (
+6 -6
View File
@@ -20,12 +20,12 @@ export default function Type(props: AvailabilityPageProps) {
const { t } = useLocale(); const { t } = useLocale();
return props.away ? ( return props.away ? (
<div className="h-screen dark:bg-gray-900"> <div className="dark:bg-inverted h-screen">
<main className="mx-auto max-w-3xl px-4 py-24"> <main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types"> <div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900"> <div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="p-8 text-center text-gray-400 dark:text-white"> <div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white"> <h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
😴{" " + t("user_away")} 😴{" " + t("user_away")}
</h2> </h2>
<p className="mx-auto max-w-md">{t("user_away_description")}</p> <p className="mx-auto max-w-md">{t("user_away_description")}</p>
@@ -39,8 +39,8 @@ export default function Type(props: AvailabilityPageProps) {
<main className="mx-auto max-w-3xl px-4 py-24"> <main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types"> <div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900"> <div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="p-8 text-center text-gray-400 dark:text-white"> <div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white"> <h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
{" " + t("unavailable")} {" " + t("unavailable")}
</h2> </h2>
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled")}</p> <p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled")}</p>
@@ -54,7 +54,7 @@ export default function Type(props: AvailabilityPageProps) {
); );
} }
Type.isThemeSupported = true; Type.isBookingPage = true;
const paramsSchema = z.object({ type: z.string(), user: z.string() }); const paramsSchema = z.object({ type: z.string(), user: z.string() });
async function getUserPageProps(context: GetStaticPropsContext) { async function getUserPageProps(context: GetStaticPropsContext) {
Loaded 100 of 714 files, more files were not shown because too many files have changed in this diff. Show more