Compare commits

..
111 Commits
Author SHA1 Message Date
Omar López ab6758194b v2.6.0
release.yml / release (push) Canceled after 0s
2023-02-15 11:28:32 -07:00
zomars 5528d60897 Set bundle threshold to 500 bytes 2023-02-15 11:05:12 -07:00
Nafees Nazik bb1c55e07f fix: booking confirmation email missing variables (#7109)
* fix: make the method public

* fix: missing variables
2023-02-15 10:49:44 -07:00
Nafees Nazik c899921591 fix: label (#7119) 2023-02-15 17:19:39 +00:00
sean-brydonandzomars ea34545d72 Handle booking flow for dynamic default events (#7094)
* Handle booking flow for default events

* Cleanup

* Update packages/lib/defaultEvents.ts

* Fix naming conventions

* Fix prettier error

---------

Co-authored-by: zomars <zomars@me.com>
2023-02-15 15:42:49 +00:00
Omar López 9d3540511b Add/eslint plugin unused imports (#7102)
* Re added debug endpoint

* Update me.ts

* Adds eslint-plugin-unused-imports

* Linting
2023-02-14 23:26:29 +00:00
zomars e8cf92361a Fingers crossed 🤞 2023-02-14 15:36:02 -07:00
zomars e8525b963b Update .gitignore 2023-02-14 14:47:58 -07:00
zomars 24dabd0ddc Update nextjs-bundle-analysis.yml 2023-02-14 14:21:12 -07:00
zomars 5203168b2e Re added debug endpoint
Update me.ts
2023-02-14 14:11:09 -07:00
Omar López c3e40f84a9 v2.5.15
release.yml / release (push) Canceled after 0s
2023-02-14 10:08:30 -07:00
Omar López 8e2ece7526 Prevent timeouts when cache fails (#7095)
Return empty cache instead of failing

Keeping console errors
2023-02-14 10:07:58 -07:00
Omar López b6eca6e083 v2.5.14
release.yml / release (push) Canceled after 0s
2023-02-14 09:02:24 -07:00
zomars 4cb264f1f7 Revert "Revert "I18N Caching (#6823)""
This reverts commit e248fda36b.

Re-fixed cache rules
2023-02-14 09:00:28 -07:00
Alex van Andel e248fda36b Revert "I18N Caching (#6823)"
This reverts commit 05c33bd94b.
2023-02-14 13:55:19 +00:00
sean-brydon 1c97e549eb Implement URL validation on default videos links (#7087) 2023-02-14 13:19:45 +00:00
77600f8168 Feat/custom default video (#6971)
* Add queries to fetch metadata and update default

* Add to zod schema

* Change default rules on settings page

* Update query

* Initall override on create event type

* Working default

* Change schema to save locationLink too

* Update default conferncing to include link

* Rename tsx to ts. Fix metadata keys

* WIP on refactoring appstore installed

* Add this to appstore installed apps too

* Display only on conferencing apps

* Add i18n strings

* Change order in dropdown menu so delete is bottom

* Refactor query to not expose all of metadata

* Fix mr type checker

* Possible e2e fix

* Refactor metadata to use ctx

* Refactor back to query cell

* Alias schema

* Adding comment for more info

* Remove constant for clarity

* Update nextjs-bundle-analysis.yml

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: zomars <zomars@me.com>
2023-02-13 22:55:32 +00:00
Omar López 6cad606ba1 v2.5.13
release.yml / release (push) Canceled after 0s
2023-02-13 15:54:14 -07:00
zomars 7cae7a7bd4 Bundle analyzer fixes 2023-02-13 15:38:44 -07:00
zomars 93f65807e7 Bundle output fixes 2023-02-13 15:13:57 -07:00
Peer Richelsen 0a63820699 fixed border on dark mode (#7079) 2023-02-13 23:02:57 +01:00
zomars 05ad437b27 Update nextjs-bundle-analysis.yml 2023-02-13 15:01:23 -07:00
zomars 26a57a71ae Update nextjs-bundle-analysis.yml 2023-02-13 14:46:04 -07:00
zomars 34a10205a3 Update nextjs-bundle-analysis.yml 2023-02-13 14:44:55 -07:00
Joe Shajan fbf8ff6db7 Public booking page hover fix (#6952)
* fix: hover bg on dark mode

* fix: badge bg color

* fix: bg darkgray 200 on dark mode
2023-02-13 22:02:12 +01:00
Amir FakhrullahandUdit Takkar bef6d18fdf [CAL-1030] fix flickering border issue in menu layout for mobile (#7036)
* fix flickering border issue in menu layout for mobile

* transfer styles changes to the DropdownMenuItem

* remove button border for Remove button

* fix: use dropdownItem

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: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-02-13 21:57:33 +01:00
zomars 2e96e2ef3f Update nextjs-bundle-analysis.yml 2023-02-13 13:54:27 -07:00
zomars 2ca6769a70 Update nextjs_bundle_analysis.yml 2023-02-13 12:44:37 -07:00
zomars 1eb514dfcf Testing bundle analyzer action 2023-02-13 12:38:54 -07:00
Nafees Nazik d6c004ba22 feat: add stale issues action (#7028) 2023-02-13 12:21:07 -07:00
+17 afb9605f0a Generate SSG Page used as cache for user's third-party calendar (#6775)
* Generate SSG Page used as cache for user's third-party calendars

* remove next-build-id dependency

* yarn.lock from main

* add support to get cached data from multiple months

* Update apps/web/pages/[user]/calendar-cache/[month].tsx

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

* Update apps/web/pages/[user]/calendar-cache/[month].tsx

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

* Update packages/core/CalendarManager.ts

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

* Add api endpoint that revalidates the current month and 3 more ahead

* Revalidate calendar cache when user connect new calendar.

* Revalidate calendar cache when user remove a calendar.

* Revalidate calendar cache when user change de selected calendars-

* Change revalidateCalendarCache function to @calcom/lib/server

* Remove the memory cache from getCachedResults

* refetch availability slots in a 3 seconds interval

* Hotfix: Event Name (#6863) (#6864)

* feat: make key unique (#6861)

* version 2.5.9 (#6868)

* Use Calendar component view=day for drop-in replacement troubleshooter (#6869)

* Use Calendar component view=day for drop-in replacement troubleshooter

* Setting the id to undefined makes the busy time selected

* Updated event title to include title+source

* lots of small changes by me and ciaran (#6871)

* Hotfix: For old Plausible installs enabled in an EventType, give a default value (#6860)

* Add default for trackingId for old plausible installs in event-types

* Fix types

* fix: filter booking in upcoming (#6406)

* fix: filter booking in upcoming

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

* fix: test

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

* fix: wipe-my-cal failing test

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>

* fix workflows when duplicating event types (#6873)

* fix: get location url from metadata (#6774)

* fix: get location url from metadata

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

* fix: replace location

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

* fix: type error

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

* fix: use zod

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

---------

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

* Updates heroku deployment template (#6879)

* Hide button (#6895)

* Fixed some inconsistencies within single event type page (#6887)

* Fixed some inconsistencies within single event type page

* Fix layout shift on AvailabilityTab

* fix(date-overrides): alignment of edit & delete btns (#6892)

* When unchecking the common schedule, schedule should be nulled (#6898)

* theme for storybook

* nit border change (#6899)

* fix: metadata not saved while creating a booking. (#6866)

* feat: add metadata to booking creation

* fix: bug

* Beginning of Strict CSP Compliance (#6841)

* Add CSP Support and enable it initially for Login page

* Update README

* Make sure that CSP is not enabled if CSP_POLICY isnt set

* Add a new value for x-csp header that tells if instance has opted-in to CSP or not

* Add more src to CSP

* Fix typo in header name

* Remove duplicate headers fn

* Add https://eu.ui-avatars.com/api/

* Add CSP_POLICY to env.example

* v2.5.10

* fix: add req.headers (#6921)

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

* add-console-vars (#6929)

* Admin Wizard Choose License (#6574)

* Implementation

* i18n

* More i18n

* extracted i18n, needs api to get most recent price, added hint: update later

* Fixing i18n var

* Fix booking filters not working for admin (#6576)

* fix: react-select overflow issue in some modals. (#6587)

* feat: add a disable overflow prop

* feat: use the disable overflow prop

* Tailwind Merge (#6596)

* Tailwind Merge

* Fix merge classNames

* [CAL-808] /availability/single - UI issue on buttons beside time inputs (#6561)

* [CAL-808] /availability/single - UI issue on buttons beside time inputs

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

* Update packages/features/schedules/components/Schedule.tsx

* create new translation for tooltip

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* Bye bye submodules (#6585)

* WIP

* Uses ssh instead

* Update .gitignore

* Update .gitignore

* Update Makefile

* Update git-setup.sh

* Update git-setup.sh

* Replaced Makefile with bash script

* Update package.json

* fix: show button on empty string (#6601)

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

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

* fix: add delete in dropdown (#6599)

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

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

* Update README.md

* Update README.md

* Changed a neutral- classes to gray (#6603)

* Changed a neutral- classes to gray

* Changed all border-1 to border

* Update package.json

* Test fixes

* Yarn lock fixes

* Fix string equality check in git-setup.sh

* [CAL-811] Avatar icon not redirecting user back to the main page (#6586)

* Remove cursor-pointer, remove old Avatar* files

* Fixed styling for checkedSelect + some cleanup

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Harsh/add member invite (#6598)

Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>

* Regenerated lockfile without upgrade (#6610)

* fix: remove annoying outline when <Button /> clicked (#6537)

* fix: remove annoying outline when <Button /> clicked

* Delete yarn.lock

* remove 1 on 1 icon (#6609)

* removed 1-on-1 badge

* changed user to users for group events

* fix: case-sensitivity in apps path (#6552)

* fix: lowercase slug

* fix: make fallback blocking

* Fix FAB (#6611)

* feat: add LocationSelect component (#6571)

* feat: add LocationSelect component

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

* fix: type error

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

* chore: type error

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

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

* Update booking filters design  (#6543)

* Update booking filters

* Add filter on YOUR bookings

* Fix pending members showing up in list

* Reduce the avatar size to 'sm' for now

* Bugfix/dropdown menu trigger as child remove class names (#6614)

* Fix UsernameTextfield to take right height

* Remove className side-effect

* Incorrect resolution version fixed

* Converted mobile DropdownMenuTrigger styles into Button

* v2.5.3

* fix: use items-center (#6618)

* fix tooltip and modal stacking issues (#6491)

* fix tooltip and modal stacking issues

* use z-index in larger screens and less

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

* Temporary fix (#6626)

* Fix Ga4 tracking (#6630)

* generic <UpgradeScreen> component (#6594)

* first attempt of <UpgradeScreen>

* changes to icons

* reverted changes back to initial state, needs fix: teams not showing

* WIP

* Fix weird reactnode error

* Fix loading text

* added upgradeTip to routing forms

* icon colors

* create and use hook to check if user has team plan

* use useTeamPlan for upgradeTeamsBadge

* replace huge svg with compressed jpeg

* responsive fixes

* Update packages/ui/components/badge/UpgradeTeamsBadge.tsx

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

* Give team plan features to E2E tests

* Allow option to make a user part of team int ests

* Remove flash of paywall for team user

* Add team user for typeform tests as well

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* Removing env var to rely on db

* Restoring i18n keys, set loading moved

* Fixing tailwind-preset glob

* Wizard width fix for md+ screens

* Converting licenses options to radix radio

* Applying feedback + other tweaks

* Reverting this, not this PR related

* Unneeded code removal

* Reverting unneeded style change

* Applying feedback

* Removing licenseType

* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* Adds deployment settings to DB (#6706)

* WIP

* Adds DeploymentTheme

* Add missing migrations

* Adds client extensions for deployment

* Cleanup

* Delete migration.sql

* Relying on both, env var and new model

* Restoring env example doc for backward compat

* Maximum call stack size exceeded fix?

* Revert upgrade

* Update index.ts

* Delete index.ts

* Not exposing license key, fixed radio behavior

* Covering undefined env var

* Self contained checkLicense

* Feedback

* Moar feedback

* Feedback

* Feedback

* Feedback

* Cleanup

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Harsh Singh <51085015+harshsinghatz@users.noreply.github.com>
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>
Co-authored-by: Luis Cadillo <luiscaf3r@gmail.com>
Co-authored-by: Mohammed Cherfaoui <hi@cherfaoui.dev>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* added two new tips (#6915)

* [CAL-488] Timezone selection has a weird double dropdown (#6851)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: color and line height of icon (#6913)

* fix: use destination calendar email (#6886)

* fix: use destination calendar email
to display correct primary email

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

* fix: simplify logic

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

---------

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

* fix: dropdown title in bookings page (#6924)

* fixes the broken max size of members on teams page (#6926)

* fix: display provider name instead of url (#6914)

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

* fix: add sortByLabel (#6797)

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

* Email Variables Bug (#6943)

* Remove _subject translations for zh-CN, needs retranslation

* minor timezone-select improvements (#6944)

* fixed timezone select positioning and hover

* fixed timezone select positioning and hover

* Give trackingId a default value because if user doesnt interact with trackingId input it is not set (#6945)

* Block /auth/:path, nothing else. (#6949)

* Block /auth/:path, nothing else.

* Also add /signup

* fix start icon in button (#6950)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* Fixes localisation of {EVENT_DATE} in workflows (#6907)

* translate {EVENT_DATE} variable to correct language

* fix locale for cron schedule reminder emails/sms

* fix type error

* add missing locale to attendees

* fix type error

* code clean up

* revert last commit

* using Intl for date translations

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* Allow account linking for Google and SAML providers (#6874)

* allow account linking for self-hosted instances, both Google and SAML are verified emails

* allow account linking for Google and SSO if emails match with existing username/password account

* Tweaked find user by email since we now have multiple providers (other than credentials provider)

* feat/payment-service-6438-cal-767 (#6677)

* WIP paymentService

* Changes for payment Service

* Fix for stripe payment flow

* Remove logs/comments

* Refactored refund for stripe app

* Move stripe handlePayment to own lib

* Move stripe delete payments to paymentService

* lint fix

* Change handleRefundError as generic function

* remove log

* remove logs

* remove logs

* Return stripe default export to lib/server

* Fixing types

* Fix types

* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* Adds deployment settings to DB (#6706)

* WIP

* Adds DeploymentTheme

* Add missing migrations

* Adds client extensions for deployment

* Cleanup

* Revert "lint fix"

This reverts commit e1a2e4a357e58e6673c47399888ae2e00d1351a6.

* Add validation

* Revert changes removed in force push

* Removing abstract class and just leaving interface implementation

* Fix types for handlePayments

* Fix payment test appStore import

* Fix stripe metadata in event type

* Move migration to separate PR

* Revert "Move migration to separate PR"

This reverts commit 48aa64e0724a522d3cc2fefaaaee5792ee9cd9e6.

* Update packages/prisma/migrations/20230125175109_remove_type_from_payment_and_add_app_relationship/migration.sql

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

---------

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

* Small UI fixes for seats & destination calendars (#6859)

* Do not add former time for events on seats

* Default display destination calendar

* Add seats badge to event type item

* Add string

* Actively watch seats enabled option for requires confirmation

* Only show former time when there is a rescheduleUid

* fix: use typedquery hook in duplicate dialog (#6730)

* fix: use typedquery hook in duplicate dialog

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

* Update packages/features/eventtypes/components/DuplicateDialog.tsx

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Omar López <zomars@me.com>

* Fixing admin wizard step done (#6954)

* Feature/maintenance mode (#6930)

* Implement maintenance mode with Vercel Edge Config

* Error log is spam during development/ added \n in .env.example

* Exclude _next, /api for /maintenance page

* Re-instate previous config

* rtl: begone

* Added note to explain why /auth/login covers the maintenance page.

---------

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

* Update package.json

* I18N Caching (#6823)

* Caching Logic Changes

Enabled this function to change its cache value based on incoming paths value

* Invalidate I18N Cache

Invalidating the I18N cache when a user saves changes to their General settings

* Removes deprecated useLocale location

* Overriding the default getSchedule cache to have a revalidation time of 1 second

* Update apps/web/pages/api/trpc/[trpc].ts

* Updated cache values to match the comment

---------

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

* feat: return `x-vercel-ip-timezone` in headers (#6849)

* feat: add trpc to matcher and pass vercel timezone header

* feat: pass request to context

* feat: return timezone in header

* refactor: split context

* fix: remove tsignore comment

* Update [trpc].ts

---------

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

* log the json url for testing

* use WEBAPP_URL constant instead env.NEXT_PUBLIC_WEBAPP_URL

* remove the commented selectedCalendars var, it is not necessary

* Caching fixes

* Separate selectedDate slots from month slots

* Update [trpc].ts

* Log headers

* Update [trpc].ts

* Update package.json

* Fixes/trpc headers (#7045)

* Cache fixes

* Testing

* SWR breaks embed tests

* Prevent refetching day on month switch

* Skeleton fixes

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: Ben Hybert <53020786+Hybes@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Sai Deepesh <saideepesh000@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Harsh Singh <51085015+harshsinghatz@users.noreply.github.com>
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>
Co-authored-by: Luis Cadillo <luiscaf3r@gmail.com>
Co-authored-by: Mohammed Cherfaoui <hi@cherfaoui.dev>
Co-authored-by: Joe Shajan <joeshajan1551@gmail.com>
Co-authored-by: Deepak Prabhakara <deepak@boxyhq.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Aaron Presley <155617+AaronPresley@users.noreply.github.com>
2023-02-13 11:42:53 -07:00
Nafees Nazik a2549bd7b6 feat: use week start from owner (#7072) 2023-02-13 16:21:55 +00:00
Joe Shajan c2a8f799ff fix: availability header margin bottom (#7037) 2023-02-13 15:06:27 +00:00
sean-brydon c68ff54e77 Feat/full custom select (#6865)
* Inital Setup - refactors/optimise

* Escape clears focus

* A11y and optimisations

* A11y enter fix

* Sort out flatternt list indexing

* Fix spacing - fix single selected state

* Solves group highlights breaking things

* Fix selecting while filtering list

* Remove styles and rename story

* Hide labels while searching for now

* Remove group item

* move onclick hook to hooks

* Fix typo

* Use check icon on single select

* scroll into view when keyboard navigating

* add comment explaining dropdown close

* Remove tenery and make disabled easier to read

* Typo

* remove log

* Remove destructing of classNames props

* Refactor callbacks + hooks

* Style updates with ciaran

* Final style changes

* center icon

* Add darkmode
2023-02-13 15:14:23 +01:00
darshan 74fe20802d Ability to write placeholder in routing forms (#7054) 2023-02-13 18:43:24 +05:30
Aaron Presley d59cda3286 Collective events are now considered from getBusyTimes booking query (#6911)
* Updated the busyTimes query to include bookings owned OR hosted by the given user

* Updated getSchedule tests to account for the new where condition

* Ignoring typing within a mocked function

* Further type checks, figured out how to run the command locally
2023-02-13 12:46:37 +00:00
Carina WollendorferandCarinaWolli 1cae2b6f3d Fixing issues around workflows (#6991)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-02-13 12:39:06 +00:00
Richard Poelderl 9d9fa8e575 turn inline logo & shell headers into h3 (#7060) 2023-02-13 12:29:48 +00:00
Nafees Nazik f04c634c48 fix: fixme line in todo action (#6982)
* fix: remove line marked with the comment fixme

* fix: remove push
2023-02-13 12:27:48 +00:00
alannnc c3e7981917 Fix/bookings unintended confirmations on rejection 6975 (#7047)
* Fixed translation message and added paymentStatus to booking page

* Fix booking confirmation with payment

* Remove previous code unnecesary
2023-02-13 12:26:28 +00:00
Shane Maglangit d6594dab0f fix: add null check for userSchedule.availability (#7053) 2023-02-13 11:23:24 +00:00
Carina WollendorferandCarinaWolli 5fcfbb2d90 fix text color (#7056)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-02-13 11:10:52 +00:00
Peer Richelsen 8a99c3d2a5 Update Tips.tsx 2023-02-12 22:34:07 +01:00
Peer Richelsen 336c2d3643 Update README.md 2023-02-12 22:06:03 +01:00
Peer Richelsen cfe3ef4ab0 Update README.md 2023-02-12 20:43:16 +01:00
Peer Richelsen 4ec8241a86 Update README.md 2023-02-12 20:33:16 +01:00
Peer Richelsen e6313aafac Update README.md 2023-02-12 20:31:32 +01:00
Peer Richelsen f87f030bb5 Update README.md 2023-02-12 20:31:20 +01:00
Amir FakhrullahandPeer Richelsen f87254cdc3 Fix: avatar group buggy with +0 (#7044)
* fix AvatarGroup, refactor a bit

* handle cases where truncated avatars are 2 digits

* use slice, and add comment

* update comment

* Update packages/ui/components/avatar/AvatarGroup.tsx

* fix classname order issue

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
2023-02-11 16:25:36 +00:00
Omar López 1f74837189 Fixes/trpc headers (#7045)
* Cache fixes

* Testing

* SWR breaks embed tests
2023-02-10 14:06:52 -07:00
Omar López c7064906c9 Update package.json (#7043) 2023-02-10 13:50:01 -07:00
Omar López 86c94a3cd5 Upgrades tRPC to 10.10.0 (#6995)
* Upgrades tRPC to 10.10.0

* Type fixes
2023-02-10 13:05:02 -07:00
Joe Au-Yeung 2056ebe186 Only show seats badge if it is greater than 1 (#7041) 2023-02-10 16:38:20 +00:00
Leo Giovanetti 53424ca9ae Fixing using given username (#7033) 2023-02-10 10:23:24 -03:00
Nafees NazikandPeer Richelsen 8197ba18d4 feat: add new Jitsu events (#6980)
Co-authored-by: Peer Richelsen <peer@cal.com>
2023-02-10 09:53:03 +00:00
GitStart-Cal.comandgitstart-calcom d341ea5111 [CAL-812] Button group focus/hover state (#6854)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-02-09 21:54:09 +00:00
Omar López 42dcd4a2a9 Revert "Use Calendar component view=day for drop-in replacement troubleshooter (#6869)" (#6985)
This reverts commit f18e80cfcf.
2023-02-09 12:01:43 -07:00
Leo Giovanetti c63225fdbf Fixing finish button not working (#6977) 2023-02-09 18:15:28 +00:00
zomars 41774e4932 Fix flaky test 2023-02-09 10:51:48 -07:00
Omar López af7849f951 v2.5.12
release.yml / release (push) Canceled after 0s
2023-02-09 09:34:51 -07:00
Alex van Andel 5353610d9a Only handle refunds when there are payments, otherwise skip (#6974) 2023-02-09 15:43:28 +00:00
+14 156f3e5de2 fix: admin password hint text (#6906)
* feat: use the trans hook

* fix: hide AdminPasswordBanner

* fix: translation

* fix: style and use validation

* fix: layout

* fix: use trans keys

* fix: replace number with placeholder

* feat: add alert banner

* fix: use the correct password length

* fix: metadata not saved while creating a booking. (#6866)

* feat: add metadata to booking creation

* fix: bug

* Beginning of Strict CSP Compliance (#6841)

* Add CSP Support and enable it initially for Login page

* Update README

* Make sure that CSP is not enabled if CSP_POLICY isnt set

* Add a new value for x-csp header that tells if instance has opted-in to CSP or not

* Add more src to CSP

* Fix typo in header name

* Remove duplicate headers fn

* Add https://eu.ui-avatars.com/api/

* Add CSP_POLICY to env.example

* v2.5.10

* fix: add req.headers (#6921)

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

* add-console-vars (#6929)

* Admin Wizard Choose License (#6574)

* Implementation

* i18n

* More i18n

* extracted i18n, needs api to get most recent price, added hint: update later

* Fixing i18n var

* Fix booking filters not working for admin (#6576)

* fix: react-select overflow issue in some modals. (#6587)

* feat: add a disable overflow prop

* feat: use the disable overflow prop

* Tailwind Merge (#6596)

* Tailwind Merge

* Fix merge classNames

* [CAL-808] /availability/single - UI issue on buttons beside time inputs (#6561)

* [CAL-808] /availability/single - UI issue on buttons beside time inputs

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

* Update packages/features/schedules/components/Schedule.tsx

* create new translation for tooltip

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* Bye bye submodules (#6585)

* WIP

* Uses ssh instead

* Update .gitignore

* Update .gitignore

* Update Makefile

* Update git-setup.sh

* Update git-setup.sh

* Replaced Makefile with bash script

* Update package.json

* fix: show button on empty string (#6601)

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

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

* fix: add delete in dropdown (#6599)

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

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

* Update README.md

* Update README.md

* Changed a neutral- classes to gray (#6603)

* Changed a neutral- classes to gray

* Changed all border-1 to border

* Update package.json

* Test fixes

* Yarn lock fixes

* Fix string equality check in git-setup.sh

* [CAL-811] Avatar icon not redirecting user back to the main page (#6586)

* Remove cursor-pointer, remove old Avatar* files

* Fixed styling for checkedSelect + some cleanup

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Harsh/add member invite (#6598)

Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>

* Regenerated lockfile without upgrade (#6610)

* fix: remove annoying outline when <Button /> clicked (#6537)

* fix: remove annoying outline when <Button /> clicked

* Delete yarn.lock

* remove 1 on 1 icon (#6609)

* removed 1-on-1 badge

* changed user to users for group events

* fix: case-sensitivity in apps path (#6552)

* fix: lowercase slug

* fix: make fallback blocking

* Fix FAB (#6611)

* feat: add LocationSelect component (#6571)

* feat: add LocationSelect component

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

* fix: type error

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

* chore: type error

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

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

* Update booking filters design  (#6543)

* Update booking filters

* Add filter on YOUR bookings

* Fix pending members showing up in list

* Reduce the avatar size to 'sm' for now

* Bugfix/dropdown menu trigger as child remove class names (#6614)

* Fix UsernameTextfield to take right height

* Remove className side-effect

* Incorrect resolution version fixed

* Converted mobile DropdownMenuTrigger styles into Button

* v2.5.3

* fix: use items-center (#6618)

* fix tooltip and modal stacking issues (#6491)

* fix tooltip and modal stacking issues

* use z-index in larger screens and less

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

* Temporary fix (#6626)

* Fix Ga4 tracking (#6630)

* generic <UpgradeScreen> component (#6594)

* first attempt of <UpgradeScreen>

* changes to icons

* reverted changes back to initial state, needs fix: teams not showing

* WIP

* Fix weird reactnode error

* Fix loading text

* added upgradeTip to routing forms

* icon colors

* create and use hook to check if user has team plan

* use useTeamPlan for upgradeTeamsBadge

* replace huge svg with compressed jpeg

* responsive fixes

* Update packages/ui/components/badge/UpgradeTeamsBadge.tsx

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

* Give team plan features to E2E tests

* Allow option to make a user part of team int ests

* Remove flash of paywall for team user

* Add team user for typeform tests as well

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* Removing env var to rely on db

* Restoring i18n keys, set loading moved

* Fixing tailwind-preset glob

* Wizard width fix for md+ screens

* Converting licenses options to radix radio

* Applying feedback + other tweaks

* Reverting this, not this PR related

* Unneeded code removal

* Reverting unneeded style change

* Applying feedback

* Removing licenseType

* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* Adds deployment settings to DB (#6706)

* WIP

* Adds DeploymentTheme

* Add missing migrations

* Adds client extensions for deployment

* Cleanup

* Delete migration.sql

* Relying on both, env var and new model

* Restoring env example doc for backward compat

* Maximum call stack size exceeded fix?

* Revert upgrade

* Update index.ts

* Delete index.ts

* Not exposing license key, fixed radio behavior

* Covering undefined env var

* Self contained checkLicense

* Feedback

* Moar feedback

* Feedback

* Feedback

* Feedback

* Cleanup

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Harsh Singh <51085015+harshsinghatz@users.noreply.github.com>
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>
Co-authored-by: Luis Cadillo <luiscaf3r@gmail.com>
Co-authored-by: Mohammed Cherfaoui <hi@cherfaoui.dev>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* added two new tips (#6915)

* [CAL-488] Timezone selection has a weird double dropdown (#6851)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: color and line height of icon (#6913)

* fix: use destination calendar email (#6886)

* fix: use destination calendar email
to display correct primary email

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

* fix: simplify logic

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

---------

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

* fix: dropdown title in bookings page (#6924)

* fixes the broken max size of members on teams page (#6926)

* fix: display provider name instead of url (#6914)

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

* fix: add sortByLabel (#6797)

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

* Email Variables Bug (#6943)

* Remove _subject translations for zh-CN, needs retranslation

* minor timezone-select improvements (#6944)

* fixed timezone select positioning and hover

* fixed timezone select positioning and hover

* Give trackingId a default value because if user doesnt interact with trackingId input it is not set (#6945)

* Block /auth/:path, nothing else. (#6949)

* Block /auth/:path, nothing else.

* Also add /signup

* fix start icon in button (#6950)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* Fixes localisation of {EVENT_DATE} in workflows (#6907)

* translate {EVENT_DATE} variable to correct language

* fix locale for cron schedule reminder emails/sms

* fix type error

* add missing locale to attendees

* fix type error

* code clean up

* revert last commit

* using Intl for date translations

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* Allow account linking for Google and SAML providers (#6874)

* allow account linking for self-hosted instances, both Google and SAML are verified emails

* allow account linking for Google and SSO if emails match with existing username/password account

* Tweaked find user by email since we now have multiple providers (other than credentials provider)

* feat/payment-service-6438-cal-767 (#6677)

* WIP paymentService

* Changes for payment Service

* Fix for stripe payment flow

* Remove logs/comments

* Refactored refund for stripe app

* Move stripe handlePayment to own lib

* Move stripe delete payments to paymentService

* lint fix

* Change handleRefundError as generic function

* remove log

* remove logs

* remove logs

* Return stripe default export to lib/server

* Fixing types

* Fix types

* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* Adds deployment settings to DB (#6706)

* WIP

* Adds DeploymentTheme

* Add missing migrations

* Adds client extensions for deployment

* Cleanup

* Revert "lint fix"

This reverts commit e1a2e4a357e58e6673c47399888ae2e00d1351a6.

* Add validation

* Revert changes removed in force push

* Removing abstract class and just leaving interface implementation

* Fix types for handlePayments

* Fix payment test appStore import

* Fix stripe metadata in event type

* Move migration to separate PR

* Revert "Move migration to separate PR"

This reverts commit 48aa64e0724a522d3cc2fefaaaee5792ee9cd9e6.

* Update packages/prisma/migrations/20230125175109_remove_type_from_payment_and_add_app_relationship/migration.sql

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

---------

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

* Small UI fixes for seats & destination calendars (#6859)

* Do not add former time for events on seats

* Default display destination calendar

* Add seats badge to event type item

* Add string

* Actively watch seats enabled option for requires confirmation

* Only show former time when there is a rescheduleUid

* fix: use typedquery hook in duplicate dialog (#6730)

* fix: use typedquery hook in duplicate dialog

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

* Update packages/features/eventtypes/components/DuplicateDialog.tsx

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Omar López <zomars@me.com>

* Fixing admin wizard step done (#6954)

* Feature/maintenance mode (#6930)

* Implement maintenance mode with Vercel Edge Config

* Error log is spam during development/ added \n in .env.example

* Exclude _next, /api for /maintenance page

* Re-instate previous config

* rtl: begone

* Added note to explain why /auth/login covers the maintenance page.

---------

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

* Update package.json

* I18N Caching (#6823)

* Caching Logic Changes

Enabled this function to change its cache value based on incoming paths value

* Invalidate I18N Cache

Invalidating the I18N cache when a user saves changes to their General settings

* Removes deprecated useLocale location

* Overriding the default getSchedule cache to have a revalidation time of 1 second

* Update apps/web/pages/api/trpc/[trpc].ts

* Updated cache values to match the comment

---------

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Harsh Singh <51085015+harshsinghatz@users.noreply.github.com>
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>
Co-authored-by: Luis Cadillo <luiscaf3r@gmail.com>
Co-authored-by: Mohammed Cherfaoui <hi@cherfaoui.dev>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Joe Shajan <joeshajan1551@gmail.com>
Co-authored-by: Ben Hybert <53020786+Hybes@users.noreply.github.com>
Co-authored-by: Deepak Prabhakara <deepak@boxyhq.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Aaron Presley <155617+AaronPresley@users.noreply.github.com>
2023-02-09 02:02:17 +00:00
Omar López 05f51459a6 Add todo to issue action (#6749)
* Create todo-to-issue.yml

* Update todo-to-issue.yml

* Update todo-to-issue.yml

* Update todo-to-issue.yml

* Update todo-to-issue.yml

* test

* Update todo-to-issue.yml
2023-02-08 18:24:01 -07:00
Nafees Nazikandzomars f0db97bbb1 feat: return x-vercel-ip-timezone in headers (#6849)
* feat: add trpc to matcher and pass vercel timezone header

* feat: pass request to context

* feat: return timezone in header

* refactor: split context

* fix: remove tsignore comment

* Update [trpc].ts

---------

Co-authored-by: zomars <zomars@me.com>
2023-02-09 01:12:45 +00:00
Aaron Presleyandzomars 05c33bd94b I18N Caching (#6823)
* Caching Logic Changes

Enabled this function to change its cache value based on incoming paths value

* Invalidate I18N Cache

Invalidating the I18N cache when a user saves changes to their General settings

* Removes deprecated useLocale location

* Overriding the default getSchedule cache to have a revalidation time of 1 second

* Update apps/web/pages/api/trpc/[trpc].ts

* Updated cache values to match the comment

---------

Co-authored-by: zomars <zomars@me.com>
2023-02-08 17:35:43 -07:00
Omar López 7d4cc59cd2 Update package.json
release.yml / release (push) Canceled after 0s
2023-02-08 16:07:19 -07:00
Alex van AndelandOmar López a94465ff04 Feature/maintenance mode (#6930)
* Implement maintenance mode with Vercel Edge Config

* Error log is spam during development/ added \n in .env.example

* Exclude _next, /api for /maintenance page

* Re-instate previous config

* rtl: begone

* Added note to explain why /auth/login covers the maintenance page.

---------

Co-authored-by: Omar López <zomars@me.com>
2023-02-08 21:51:58 +00:00
Leo Giovanetti 4d54ac3ca2 Fixing admin wizard step done (#6954) 2023-02-08 14:36:46 -07:00
15da690270 fix: use typedquery hook in duplicate dialog (#6730)
* fix: use typedquery hook in duplicate dialog

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

* Update packages/features/eventtypes/components/DuplicateDialog.tsx

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Omar López <zomars@me.com>
2023-02-08 21:29:16 +00:00
Joe Au-Yeung 2462e8e597 Small UI fixes for seats & destination calendars (#6859)
* Do not add former time for events on seats

* Default display destination calendar

* Add seats badge to event type item

* Add string

* Actively watch seats enabled option for requires confirmation

* Only show former time when there is a rescheduleUid
2023-02-08 21:13:10 +00:00
2f628a17df feat/payment-service-6438-cal-767 (#6677)
* WIP paymentService

* Changes for payment Service

* Fix for stripe payment flow

* Remove logs/comments

* Refactored refund for stripe app

* Move stripe handlePayment to own lib

* Move stripe delete payments to paymentService

* lint fix

* Change handleRefundError as generic function

* remove log

* remove logs

* remove logs

* Return stripe default export to lib/server

* Fixing types

* Fix types

* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* Adds deployment settings to DB (#6706)

* WIP

* Adds DeploymentTheme

* Add missing migrations

* Adds client extensions for deployment

* Cleanup

* Revert "lint fix"

This reverts commit e1a2e4a357e58e6673c47399888ae2e00d1351a6.

* Add validation

* Revert changes removed in force push

* Removing abstract class and just leaving interface implementation

* Fix types for handlePayments

* Fix payment test appStore import

* Fix stripe metadata in event type

* Move migration to separate PR

* Revert "Move migration to separate PR"

This reverts commit 48aa64e0724a522d3cc2fefaaaee5792ee9cd9e6.

* Update packages/prisma/migrations/20230125175109_remove_type_from_payment_and_add_app_relationship/migration.sql

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

---------

Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-02-08 13:36:22 -07:00
Deepak Prabhakara 1ee3783db3 Allow account linking for Google and SAML providers (#6874)
* allow account linking for self-hosted instances, both Google and SAML are verified emails

* allow account linking for Google and SSO if emails match with existing username/password account

* Tweaked find user by email since we now have multiple providers (other than credentials provider)
2023-02-08 11:39:56 -07:00
Carina WollendorferandCarinaWolli b3d8d7e944 Fixes localisation of {EVENT_DATE} in workflows (#6907)
* translate {EVENT_DATE} variable to correct language

* fix locale for cron schedule reminder emails/sms

* fix type error

* add missing locale to attendees

* fix type error

* code clean up

* revert last commit

* using Intl for date translations

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-02-08 17:06:25 +00:00
Carina WollendorferandCarinaWolli a4b7f1af65 fix start icon in button (#6950)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-02-08 17:02:08 +00:00
Alex van Andel f7445bf8a1 Block /auth/:path, nothing else. (#6949)
* Block /auth/:path, nothing else.

* Also add /signup
2023-02-08 16:27:18 +00:00
Hariom Balhara 9cad7086d7 Give trackingId a default value because if user doesnt interact with trackingId input it is not set (#6945) 2023-02-08 16:20:13 +00:00
Peer Richelsen 3345ec61ee minor timezone-select improvements (#6944)
* fixed timezone select positioning and hover

* fixed timezone select positioning and hover
2023-02-08 14:34:17 +00:00
Alex van Andel 7797421ce9 Remove _subject translations for zh-CN, needs retranslation 2023-02-08 14:31:29 +00:00
Udit Takkar 075a274207 Email Variables Bug (#6943) 2023-02-08 15:17:45 +01:00
Udit Takkar a46a31040a fix: add sortByLabel (#6797)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-02-08 12:48:47 +00:00
Udit Takkar 7a28e1d6f4 fix: display provider name instead of url (#6914)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-02-08 12:45:09 +00:00
Ben Hybert 51fd7fdb3e fixes the broken max size of members on teams page (#6926) 2023-02-08 12:44:28 +00:00
Joe Shajan d01963303a fix: dropdown title in bookings page (#6924) 2023-02-08 12:43:45 +00:00
Udit Takkar 6877d0f693 fix: use destination calendar email (#6886)
* fix: use destination calendar email
to display correct primary email

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

* fix: simplify logic

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-02-08 12:19:02 +00:00
Udit Takkar 39040fca9a fix: color and line height of icon (#6913) 2023-02-08 13:14:52 +01:00
GitStart-Cal.comandgitstart-calcom 6fa4befc9b [CAL-488] Timezone selection has a weird double dropdown (#6851)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-02-08 12:08:00 +00:00
Peer Richelsen 8c9871c6ce added two new tips (#6915) 2023-02-08 11:21:07 +00:00
+8 a9af2fb255 Admin Wizard Choose License (#6574)
* Implementation

* i18n

* More i18n

* extracted i18n, needs api to get most recent price, added hint: update later

* Fixing i18n var

* Fix booking filters not working for admin (#6576)

* fix: react-select overflow issue in some modals. (#6587)

* feat: add a disable overflow prop

* feat: use the disable overflow prop

* Tailwind Merge (#6596)

* Tailwind Merge

* Fix merge classNames

* [CAL-808] /availability/single - UI issue on buttons beside time inputs (#6561)

* [CAL-808] /availability/single - UI issue on buttons beside time inputs

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

* Update packages/features/schedules/components/Schedule.tsx

* create new translation for tooltip

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* Bye bye submodules (#6585)

* WIP

* Uses ssh instead

* Update .gitignore

* Update .gitignore

* Update Makefile

* Update git-setup.sh

* Update git-setup.sh

* Replaced Makefile with bash script

* Update package.json

* fix: show button on empty string (#6601)

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

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

* fix: add delete in dropdown (#6599)

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

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

* Update README.md

* Update README.md

* Changed a neutral- classes to gray (#6603)

* Changed a neutral- classes to gray

* Changed all border-1 to border

* Update package.json

* Test fixes

* Yarn lock fixes

* Fix string equality check in git-setup.sh

* [CAL-811] Avatar icon not redirecting user back to the main page (#6586)

* Remove cursor-pointer, remove old Avatar* files

* Fixed styling for checkedSelect + some cleanup

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Harsh/add member invite (#6598)

Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>

* Regenerated lockfile without upgrade (#6610)

* fix: remove annoying outline when <Button /> clicked (#6537)

* fix: remove annoying outline when <Button /> clicked

* Delete yarn.lock

* remove 1 on 1 icon (#6609)

* removed 1-on-1 badge

* changed user to users for group events

* fix: case-sensitivity in apps path (#6552)

* fix: lowercase slug

* fix: make fallback blocking

* Fix FAB (#6611)

* feat: add LocationSelect component (#6571)

* feat: add LocationSelect component

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

* fix: type error

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

* chore: type error

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

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

* Update booking filters design  (#6543)

* Update booking filters

* Add filter on YOUR bookings

* Fix pending members showing up in list

* Reduce the avatar size to 'sm' for now

* Bugfix/dropdown menu trigger as child remove class names (#6614)

* Fix UsernameTextfield to take right height

* Remove className side-effect

* Incorrect resolution version fixed

* Converted mobile DropdownMenuTrigger styles into Button

* v2.5.3

* fix: use items-center (#6618)

* fix tooltip and modal stacking issues (#6491)

* fix tooltip and modal stacking issues

* use z-index in larger screens and less

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

* Temporary fix (#6626)

* Fix Ga4 tracking (#6630)

* generic <UpgradeScreen> component (#6594)

* first attempt of <UpgradeScreen>

* changes to icons

* reverted changes back to initial state, needs fix: teams not showing

* WIP

* Fix weird reactnode error

* Fix loading text

* added upgradeTip to routing forms

* icon colors

* create and use hook to check if user has team plan

* use useTeamPlan for upgradeTeamsBadge

* replace huge svg with compressed jpeg

* responsive fixes

* Update packages/ui/components/badge/UpgradeTeamsBadge.tsx

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

* Give team plan features to E2E tests

* Allow option to make a user part of team int ests

* Remove flash of paywall for team user

* Add team user for typeform tests as well

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* Removing env var to rely on db

* Restoring i18n keys, set loading moved

* Fixing tailwind-preset glob

* Wizard width fix for md+ screens

* Converting licenses options to radix radio

* Applying feedback + other tweaks

* Reverting this, not this PR related

* Unneeded code removal

* Reverting unneeded style change

* Applying feedback

* Removing licenseType

* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* Adds deployment settings to DB (#6706)

* WIP

* Adds DeploymentTheme

* Add missing migrations

* Adds client extensions for deployment

* Cleanup

* Delete migration.sql

* Relying on both, env var and new model

* Restoring env example doc for backward compat

* Maximum call stack size exceeded fix?

* Revert upgrade

* Update index.ts

* Delete index.ts

* Not exposing license key, fixed radio behavior

* Covering undefined env var

* Self contained checkLicense

* Feedback

* Moar feedback

* Feedback

* Feedback

* Feedback

* Cleanup

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Harsh Singh <51085015+harshsinghatz@users.noreply.github.com>
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>
Co-authored-by: Luis Cadillo <luiscaf3r@gmail.com>
Co-authored-by: Mohammed Cherfaoui <hi@cherfaoui.dev>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-02-07 17:23:42 -07:00
alannnc 8c150264f4 add-console-vars (#6929) 2023-02-07 15:40:38 -07:00
Udit Takkar cfc3644d51 fix: add req.headers (#6921)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-02-07 12:42:47 +00:00
Omar López bdb4a7353b v2.5.10
release.yml / release (push) Canceled after 0s
2023-02-06 16:12:11 -07:00
Hariom Balhara 30c0e6d1d7 Beginning of Strict CSP Compliance (#6841)
* Add CSP Support and enable it initially for Login page

* Update README

* Make sure that CSP is not enabled if CSP_POLICY isnt set

* Add a new value for x-csp header that tells if instance has opted-in to CSP or not

* Add more src to CSP

* Fix typo in header name

* Remove duplicate headers fn

* Add https://eu.ui-avatars.com/api/

* Add CSP_POLICY to env.example
2023-02-06 22:50:08 +00:00
Nafees Nazik 390967990a fix: metadata not saved while creating a booking. (#6866)
* feat: add metadata to booking creation

* fix: bug
2023-02-06 18:05:30 +00:00
Peer Richelsen cc56602474 nit border change (#6899) 2023-02-06 15:59:20 +01:00
Peer Richelsen 43a044d59e theme for storybook 2023-02-06 15:15:26 +01:00
Alex van Andel 5b8ed9c3ef When unchecking the common schedule, schedule should be nulled (#6898) 2023-02-06 15:01:54 +01:00
Sai Deepesh c3d6df2dcf fix(date-overrides): alignment of edit & delete btns (#6892) 2023-02-06 01:31:05 +00:00
Alex van Andel 9d02cb493c Fixed some inconsistencies within single event type page (#6887)
* Fixed some inconsistencies within single event type page

* Fix layout shift on AvailabilityTab
2023-02-06 00:09:27 +01:00
sean-brydon 26ed478e50 Hide button (#6895) 2023-02-05 20:30:51 +00:00
Syed Ali Shahbaz 09e338970d Updates heroku deployment template (#6879) 2023-02-05 14:50:04 +00:00
Udit Takkar f60399ca8b fix: get location url from metadata (#6774)
* fix: get location url from metadata

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

* fix: replace location

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

* fix: type error

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

* fix: use zod

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-02-05 14:43:37 +00:00
Carina Wollendorfer 5cf7e32852 fix workflows when duplicating event types (#6873) 2023-02-04 13:06:37 +01:00
24be8a63f2 fix: filter booking in upcoming (#6406)
* fix: filter booking in upcoming

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

* fix: test

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

* fix: wipe-my-cal failing test

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2023-02-03 22:03:35 +00:00
Hariom Balhara 75509c97fd Hotfix: For old Plausible installs enabled in an EventType, give a default value (#6860)
* Add default for trackingId for old plausible installs in event-types

* Fix types
2023-02-03 21:38:06 +00:00
Peer Richelsen 9b26c7aee6 lots of small changes by me and ciaran (#6871) 2023-02-03 16:49:33 +00:00
Alex van Andel f18e80cfcf Use Calendar component view=day for drop-in replacement troubleshooter (#6869)
* Use Calendar component view=day for drop-in replacement troubleshooter

* Setting the id to undefined makes the busy time selected

* Updated event title to include title+source
2023-02-03 16:43:52 +00:00
Ben Hybert 0a4b1ca014 version 2.5.9 (#6868) 2023-02-03 15:36:31 +00:00
Nafees Nazik 9fa6aced33 feat: make key unique (#6861)
release.yml / release (push) Canceled after 0s
2023-02-03 11:20:45 +01:00
Hariom Balhara 9093909075 Hotfix: Event Name (#6863) (#6864) 2023-02-03 11:20:10 +01:00
Aaron Presley 832fce91e0 Left / Right buttons now bottom-aligned and slightly shorter (#6857)
Fixes https://github.com/calcom/cal.com/issues/6589
2023-02-02 20:49:20 +01:00
Peer Richelsen 6da41fce63 fixed availabilty layout 2023-02-02 19:28:46 +01:00
219 changed files with 5023 additions and 1643 deletions
+11 -4
View File
@@ -1,20 +1,21 @@
# ********** INDEX **********
#
# - LICENSE
# - LICENSE (DEPRECATED)
# - DATABASE
# - SHARED
# - NEXTAUTH
# - E-MAIL SETTINGS
# - LICENSE *************************************************************************************************
# https://github.com/calendso/calendso/blob/main/LICENSE
# - LICENSE (DEPRECATED) ************************************************************************************
# https://github.com/calcom/cal.com/blob/main/LICENSE
#
# Summary of terms:
# - The codebase has to stay open source, whether it was modified or not
# - You can not repackage or sell the codebase
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
#
# To enable enterprise-only features, fill your license key in here.
# To enable enterprise-only features, as an admin, go to /auth/setup to select your license and follow
# instructions. This environment variable is deprecated although still supported for backward compatibility.
# @see https://console.cal.com
CALCOM_LICENSE_KEY=
# ***********************************************************************************************************
@@ -157,3 +158,9 @@ NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
# Set this to true in to disable new signups
# NEXT_PUBLIC_DISABLE_SIGNUP=true
NEXT_PUBLIC_DISABLE_SIGNUP=
# Content Security Policy
CSP_POLICY=
# Vercel Edge Config
EDGE_CONFIG=
+22
View File
@@ -0,0 +1,22 @@
name: Cron - mark stale for inactive issues
permissions:
issues: write
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs every day (see https://crontab.guru)
- cron: "0 0 * * *"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v7
with:
days-before-stale: 60
include-only-assigned: true
days-before-close: -1
@@ -0,0 +1,153 @@
name: "Next.js Bundle Analysis"
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
defaults:
run:
# change this if your nextjs app does not live at the root of the repo
working-directory: ./apps/web/
jobs:
analyze:
env:
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
NEXTAUTH_SECRET: secret
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: true
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ secrets.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: 1
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PRO_PLAN_PRODUCT_ID }}
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_FREE_PLAN_PRODUCT_ID }}
PAYMENT_FEE_PERCENTAGE: 0.005
PAYMENT_FEE_FIXED: 10
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
SAML_ADMINS: pro@example.com
NEXTAUTH_URL: http://localhost:3000/api/auth
NEXT_PUBLIC_IS_E2E: 1
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
services:
postgres:
image: postgres:12.1
env:
POSTGRES_USER: postgres
POSTGRES_DB: calendso
ports:
- 5432:5432
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up node
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "yarn"
- name: Install dependencies
uses: bahmutov/npm-install@v1
- name: Restore next build
uses: actions/cache@v2
id: restore-build-cache
env:
cache-name: cache-next-build
with:
path: apps/web/.next/cache
# change this if you prefer a more strict cache
key: ${{ runner.os }}-build-${{ env.cache-name }}
- name: Build next.js app
# change this if your site requires a custom build command
run: cd ../.. && yarn db-seed && yarn build
# Here's the first place where next-bundle-analysis' own script is used
# This step pulls the raw bundle stats for the current bundle
- name: Analyze bundle
run: npx -p nextjs-bundle-analysis report
- name: Upload bundle
uses: actions/upload-artifact@v2
with:
name: bundle
path: apps/web/.next/analyze/__bundle_analysis.json
- name: Download base branch bundle stats
uses: dawidd6/action-download-artifact@v2
if: success() && github.event.number
with:
workflow: nextjs-bundle-analysis.yml
branch: ${{ github.event.pull_request.base.ref }}
path: apps/web/.next/analyze/base
# And here's the second place - this runs after we have both the current and
# base branch bundle stats, and will compare them to determine what changed.
# There are two configurable arguments that come from package.json:
#
# - budget: optional, set a budget (bytes) against which size changes are measured
# it's set to 350kb here by default, as informed by the following piece:
# https://infrequently.org/2021/03/the-performance-inequality-gap/
#
# - red-status-percentage: sets the percent size increase where you get a red
# status indicator, defaults to 20%
#
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
# entry in your package.json file.
- name: Compare with base branch bundle
if: success() && github.event.number
run: ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
- name: Get comment body
id: get-comment-body
if: success() && github.event.number
run: |
body=$(cat .next/analyze/__bundle_analysis_comment.txt)
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find Comment
uses: peter-evans/find-comment@v1
if: success() && github.event.number
id: fc
with:
issue-number: ${{ github.event.number }}
body-includes: "<!-- __NEXTJS_BUNDLE -->"
- name: Create Comment
uses: peter-evans/create-or-update-comment@v1.4.4
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
with:
issue-number: ${{ github.event.number }}
body: ${{ steps.get-comment-body.outputs.body }}
- name: Update Comment
uses: peter-evans/create-or-update-comment@v1.4.4
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
with:
issue-number: ${{ github.event.number }}
body: ${{ steps.get-comment-body.outputs.body }}
comment-id: ${{ steps.fc.outputs.comment-id }}
edit-mode: replace
+32
View File
@@ -0,0 +1,32 @@
name: TODO to Issue
on:
pull_request_target: # So we can test on forks
branches:
- main
workflow_dispatch:
inputs:
MANUAL_COMMIT_REF:
description: "The SHA of the commit to get the diff for"
required: true
MANUAL_BASE_REF:
description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here"
required: false
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
todos:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
- name: "Add TODOs to issues"
uses: "alstr/todo-to-issue-action@v4"
env:
MANUAL_COMMIT_REF: ${{ inputs.MANUAL_COMMIT_REF }}
MANUAL_BASE_REF: ${{ inputs.MANUAL_BASE_REF }}
with:
IDENTIFIERS: '[{"name": "TODO", "labels": ["🙋🏻‍♂️help wanted"]}, {"name": "FIXME", "labels": ["🐛 bug"]}]'
+3
View File
@@ -77,6 +77,9 @@ lint-results
#Storybook
apps/storybook/build-storybook.log
# Infisical
.infisical.json
# Snaplet
.snaplet/snapshots
.snaplet/structure.d.ts
+14 -2
View File
File diff suppressed because one or more lines are too long
+7 -4
View File
@@ -6,7 +6,7 @@
"keywords": ["react", "typescript", "node", "nextjs", "prisma", "postgres", "trpc"],
"addons": [
{
"plan": "heroku-postgresql:hobby-dev"
"plan": "heroku-postgresql:mini"
}
],
"env": {
@@ -43,15 +43,18 @@
},
"SENDGRID_API_KEY": {
"description": "Sendgrid api key. Used for email reminders in workflows",
"value": ""
"value": "",
"required": false
},
"SENDGRID_SYNC_API_KEY": {
"description": "Sendgrid internal sync service",
"value": ""
"value": "",
"required": false
},
"SENDGRID_EMAIL": {
"description": "Sendgrid email. Used for email reminders in workflows",
"value": ""
"value": "",
"required": false
},
"EMAIL_FROM": {
"description": "Configures the global From: header whilst sending emails",
+12
View File
@@ -0,0 +1,12 @@
import { create } from "@storybook/theming";
export default create({
base: "light",
colorPrimary: "#111827",
colorSecondary: "#111827",
appContentBg: "#F9FAFC",
brandTitle: "Cal.com UI",
brandUrl: "https://cal.com",
brandImage: "https://cal.com/logo.svg",
brandTarget: "_self",
});
+7
View File
@@ -0,0 +1,7 @@
import { addons } from "@storybook/addons";
import yourTheme from "./YourTheme";
addons.setConfig({
theme: yourTheme,
});
-1
View File
@@ -65,7 +65,6 @@ export default function AppListCard(props: AppListCardProps) {
<div className={`p-4 ${highlight ? "bg-yellow-100" : ""}`}>
<div className="flex gap-x-3">
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
<div className="flex grow flex-col gap-y-1 truncate">
<div className="flex items-center gap-x-2">
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
-1
View File
@@ -12,7 +12,6 @@ import {
DialogClose,
DialogContent,
HorizontalTabs,
InputLeading,
Label,
showToast,
Switch,
+1 -1
View File
@@ -1,11 +1,11 @@
import { FormEvent, useCallback, useEffect, useState } from "react";
import Cropper from "react-easy-crop";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui";
import { Area, getCroppedImg } from "@lib/cropImage";
import { useFileReader } from "@lib/hooks/useFileReader";
import { useLocale } from "@lib/hooks/useLocale";
import Slider from "@components/Slider";
@@ -10,15 +10,15 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
const { t } = useLocale();
return locations.length ? (
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
{locations.map((location) => {
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
{locations.map((location, index) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
// It's possible that the location app got uninstalled
return null;
}
return (
<div key={location.type} 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" ? (
<FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
) : (
@@ -26,7 +26,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
src={eventLocationType.iconUrl}
className={classNames(
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
)}
alt={`${eventLocationType.label} icon`}
/>
+16 -18
View File
@@ -17,13 +17,12 @@ import {
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
MeetingTimeInTimezones,
showToast,
TextArea,
Tooltip,
ActionType,
TableActions,
TextAreaField,
} from "@calcom/ui";
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
@@ -53,7 +52,7 @@ function BookingListItem(booking: BookingItemProps) {
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
const mutation = trpc.viewer.bookings.confirm.useMutation({
onSuccess: (data) => {
if (data.status === BookingStatus.REJECTED) {
if (data?.status === BookingStatus.REJECTED) {
setRejectionDialogIsOpen(false);
showToast(t("booking_rejection_success"), "success");
} else {
@@ -245,24 +244,23 @@ function BookingListItem(booking: BookingItemProps) {
)}
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
<DialogContent>
<DialogHeader title={t("rejection_reason_title")} />
<p className="-mt-4 text-sm text-gray-500">{t("rejection_reason_description")}</p>
<p className="mt-6 mb-2 text-sm font-bold text-black">
{t("rejection_reason")}
<span className="font-normal text-gray-500"> (Optional)</span>
</p>
<TextArea
name={t("rejection_reason")}
value={rejectionReason}
onChange={(e) => setRejectionReason(e.target.value)}
className="mb-5 sm:mb-6"
/>
<DialogContent title={t("rejection_reason_title")} description={t("rejection_reason_description")}>
<div>
<TextAreaField
name="rejectionReason"
label={
<>
{t("rejection_reason")}
<span className="font-normal text-gray-500"> (Optional)</span>
</>
}
value={rejectionReason}
onChange={(e) => setRejectionReason(e.target.value)}
/>
</div>
<DialogFooter>
<DialogClose />
<Button
disabled={mutation.isLoading}
onClick={() => {
+59 -14
View File
@@ -1,8 +1,10 @@
import { FC, useEffect, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { ITimezoneOption, TimezoneSelect } from "@calcom/ui";
import useMeQuery from "@lib/hooks/useMeQuery";
import { timeZone } from "../../lib/clock";
type Props = {
@@ -11,7 +13,8 @@ type Props = {
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
const [selectedTimeZone, setSelectedTimeZone] = useState("");
const { t } = useLocale();
const query = useMeQuery();
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
useEffect(() => {
setSelectedTimeZone(timeZone());
@@ -23,18 +26,60 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
}
}, [selectedTimeZone, onSelectTimeZone]);
return selectedTimeZone !== "" ? (
<div className="dark:border-darkgray-300 dark:bg-darkgray-200 rounded-sm border border-gray-200 bg-white px-4 pt-4 pb-3 shadow-sm">
<div className="mb-4 flex">
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
</div>
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
className="focus:border-brand mt-1 mb-2 block w-full rounded-md border-gray-300 text-sm focus:ring-black"
/>
</div>
const customStyles = {
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
padding: "10px 12px !important",
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
...(userTheme === "dark" && {
color: state.isSelected || state.isFocused ? "white" : "#80868B",
}),
}),
control: () => ({
display: "flex",
cursor: "pointer",
backgroundColor: "transparent !important",
minWidth: "5rem",
height: "24px",
minHeight: "24px !important",
boxShadow: "none !important",
}),
singleValue: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
...provided,
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
...(userTheme === "dark" && {
color: "#a5a5a5 !important",
}),
}),
menu: (provided: object) => ({
...provided,
minWidth: "20rem",
margin: "8px 0 0 -20px",
}),
valueContainer: (provided: object) => ({
...provided,
padding: "0 0 0 6px",
}),
dropdownIndicator: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
...provided,
transform: state.selectProps.menuIsOpen ? "rotate(180deg)" : "",
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
marginLeft: 4,
padding: 0,
...(userTheme === "dark" && {
color: "#80868B",
}),
}),
};
return !!selectedTimeZone ? (
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
className="flex h-6 text-sm font-medium"
styles={customStyles}
/>
) : null;
};
@@ -1,8 +1,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { EventType } from "@prisma/client";
import * as Popover from "@radix-ui/react-popover";
import { useRouter } from "next/router";
import { useReducer, useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useReducer, useState } from "react";
import { Toaster } from "react-hot-toast";
import { FormattedNumber, IntlProvider } from "react-intl";
import { z } from "zod";
@@ -20,7 +19,7 @@ import {
import DatePicker from "@calcom/features/calendars/DatePicker";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import notEmpty from "@calcom/lib/notEmpty";
@@ -30,7 +29,7 @@ import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import { HeadSeo } from "@calcom/ui";
import { FiChevronDown, FiChevronUp, FiCreditCard, FiGlobe, FiRefreshCcw } from "@calcom/ui/components/icon";
import { FiCreditCard, FiGlobe, FiRefreshCcw } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -53,6 +52,7 @@ const useSlots = ({
usernameList,
timeZone,
duration,
enabled = true,
}: {
eventTypeId: number;
eventTypeSlug: string;
@@ -61,6 +61,7 @@ const useSlots = ({
usernameList: string[];
timeZone?: string;
duration?: string;
enabled?: boolean;
}) => {
const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery(
{
@@ -73,19 +74,14 @@ const useSlots = ({
duration,
},
{
enabled: !!startTime && !!endTime,
enabled: !!startTime && !!endTime && enabled,
refetchInterval: 3000,
trpc: { context: { skipBatch: true } },
}
);
const [cachedSlots, setCachedSlots] = useState<NonNullable<typeof data>["slots"]>({});
useEffect(() => {
if (data?.slots) {
setCachedSlots((c) => ({ ...c, ...data?.slots }));
}
}, [data]);
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
return { slots: cachedSlots, isLoading: isLoading || isPaused };
return { slots: data?.slots || {}, isLoading: isLoading || isPaused };
};
const SlotPicker = ({
@@ -147,16 +143,7 @@ const SlotPicker = ({
}, [router.isReady, month, date, duration, timeZone]);
const { i18n, isLocaleReady } = useLocale();
const { slots: _1 } = useSlots({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
startTime: selectedDate?.startOf("day"),
endTime: selectedDate?.endOf("day"),
timeZone,
duration,
});
const { slots: _2, isLoading } = useSlots({
const { slots: monthSlots, isLoading } = useSlots({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
@@ -168,8 +155,25 @@ const SlotPicker = ({
timeZone,
duration,
});
const { slots: selectedDateSlots, isLoading: _isLoadingSelectedDateSlots } = useSlots({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
startTime: selectedDate?.startOf("day"),
endTime: selectedDate?.endOf("day"),
timeZone,
duration,
/** Prevent refetching is we already have this data from month slots */
enabled: !!selectedDate,
});
const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]);
/** Hide skeleton if we have the slot loaded in the month query */
const isLoadingSelectedDateSlots = (() => {
if (!selectedDate) return _isLoadingSelectedDateSlots;
if (!!selectedDateSlots[selectedDate.format("YYYY-MM-DD")]) return false;
if (!!monthSlots[selectedDate.format("YYYY-MM-DD")]) return false;
return false;
})();
return (
<>
@@ -179,7 +183,7 @@ const SlotPicker = ({
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 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"
)}
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
locale={isLocaleReady ? i18n.language : "en"}
selected={selectedDate}
onChange={(newDate) => {
@@ -194,8 +198,12 @@ const SlotPicker = ({
<div ref={slotPickerRef}>
<AvailableTimes
isLoading={isLoading}
slots={selectedDate && slots[selectedDate.format("YYYY-MM-DD")]}
isLoading={isLoadingSelectedDateSlots}
slots={
selectedDate &&
(selectedDateSlots[selectedDate.format("YYYY-MM-DD")] ||
monthSlots[selectedDate.format("YYYY-MM-DD")])
}
date={selectedDate}
timeFormat={timeFormat}
onTimeFormatChange={onTimeFormatChange}
@@ -212,41 +220,22 @@ const SlotPicker = ({
function TimezoneDropdown({
onChangeTimeZone,
timeZone,
}: {
onChangeTimeZone: (newTimeZone: string) => void;
timeZone?: string;
}) {
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
const handleSelectTimeZone = (newTimeZone: string) => {
onChangeTimeZone(newTimeZone);
localStorageTimeZone(newTimeZone);
setIsTimeOptionsOpen(false);
};
return (
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
<p className="flex items-center text-sm font-medium">
<FiGlobe className="min-h-4 min-w-4 ml-[2px] -mt-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]" />
{timeZone}
{isTimeOptionsOpen ? (
<FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
) : (
<FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
)}
</p>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
hideWhenDetached
align="start"
className="animate-fade-in-up absolute left-0 top-2 w-80 max-w-[calc(100vw_-_1.5rem)]">
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
<>
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white">
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</div>
</>
);
}
@@ -311,7 +300,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
[timeZone]
);
const stripeAppData = getStripeAppData(eventType);
const paymentAppData = getPaymentAppData(eventType);
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
const rawSlug = profile.slug ? profile.slug.split("/") : [];
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
@@ -362,10 +351,10 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
isBackgroundTransparent
? ""
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
"border-bookinglightest overflow-hidden md:rounded-md md:border",
"border-bookinglightest md:rounded-md md:border",
isEmbed && "mx-auto"
)}>
<div className="overflow-hidden md:flex">
<div className="md:flex">
{showEventTypeDetails && (
<div
className={classNames(
@@ -399,14 +388,14 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
</div>
</div>
)}
{stripeAppData.price > 0 && (
{paymentAppData.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium">
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en">
<FormattedNumber
value={stripeAppData.price / 100.0}
value={paymentAppData.price / 100.0}
style="currency"
currency={stripeAppData.currency.toUpperCase()}
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
</p>
@@ -6,7 +6,7 @@ import { useSession } from "next-auth/react";
import Head from "next/head";
import { useRouter } from "next/router";
import { useEffect, useMemo, useReducer, useState } from "react";
import { useFieldArray, useForm, useWatch, Controller } from "react-hook-form";
import { useFieldArray, useForm, useWatch } from "react-hook-form";
import { FormattedNumber, IntlProvider } from "react-intl";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
@@ -32,7 +32,7 @@ import {
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import { APP_NAME } from "@calcom/lib/constants";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { HttpError } from "@calcom/lib/http-error";
@@ -122,7 +122,7 @@ const BookingPage = ({
}),
{}
);
const stripeAppData = getStripeAppData(eventType);
const paymentAppData = getPaymentAppData(eventType);
// Define duration now that we support multiple duration eventTypes
let duration = eventType.length;
if (
@@ -148,6 +148,7 @@ const BookingPage = ({
const mutation = useMutation(createBooking, {
onSuccess: async (responseData) => {
const { uid, paymentUid } = responseData;
if (paymentUid) {
return await router.push(
createPaymentLink({
@@ -166,7 +167,7 @@ const BookingPage = ({
isSuccessBookingPage: true,
email: bookingForm.getValues("email"),
eventTypeSlug: eventType.slug,
formerTime: booking?.startTime.toString(),
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
},
});
},
@@ -557,14 +558,14 @@ const BookingPage = ({
{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">
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
{stripeAppData.price > 0 && (
{paymentAppData.price > 0 && (
<p className="text-bookinglight -ml-2 px-2 text-sm ">
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en">
<FormattedNumber
value={stripeAppData.price / 100.0}
value={paymentAppData.price / 100.0}
style="currency"
currency={stripeAppData.currency.toUpperCase()}
currency={paymentAppData?.currency?.toUpperCase()}
/>
</IntlProvider>
</p>
@@ -99,100 +99,105 @@ const format = (date: Date, hour12: boolean) =>
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
);
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
const EventTypeScheduleDetails = () => {
const { data: loggedInUser } = useMeQuery();
const timeFormat = loggedInUser?.timeFormat;
const { t, i18n } = useLocale();
const { watch } = useFormContext<FormValues>();
const EventTypeSchedule = () => {
const me = useMeQuery();
const timeFormat = me?.data?.timeFormat;
return (
<div className="space-y-4">
<div>
<div className="min-w-4 mb-2">
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-gray-700">
{t("availability")}
</label>
</div>
<Controller
name="schedule"
render={({ field }) => (
<AvailabilitySelect
value={field.value}
onBlur={field.onBlur}
name={field.name}
onChange={(selected) => {
field.onChange(selected?.value || null);
}}
/>
)}
/>
</div>
<div className="space-y-4 rounded border p-4 py-6 pt-2 md:p-8">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
return (
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span
className={classNames(
"w-20 font-medium sm:w-32",
!isAvailable && "text-gray-500 opacity-50"
)}>
{day}
</span>
{isLoading ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="flex items-center leading-4">
<span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ltr:ml-4 rtl:mr-4">-</span>
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
</div>
) : (
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
<Button
href={`/availability/${schedule?.schedule.id}`}
color="minimal"
EndIcon={FiExternalLink}
target="_blank"
className="justify-center border sm:border-0"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
</div>
</div>
</div>
);
};
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId });
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
);
const filterDays = (dayNum: number) =>
schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
if (!isTeamEvent) {
return <EventTypeSchedule />;
}
return (
<div className="space-y-4 rounded border px-6 pb-4">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
return (
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span
className={classNames(
"w-20 font-medium sm:w-32",
!isAvailable && "text-gray-500 opacity-50"
)}>
{day}
</span>
{isLoading ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="flex items-center leading-4">
<span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ltr:ml-4 rtl:mr-4">-</span>
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
</div>
) : (
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
<Button
href={`/availability/${schedule?.schedule.id}`}
color="minimal"
EndIcon={FiExternalLink}
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
</div>
</div>
);
};
const EventTypeSchedule = () => {
const { t } = useLocale();
return (
<div className="space-y-4">
<div>
<label htmlFor="availability" className="mb-2 block text-sm font-medium leading-none text-gray-700">
{t("availability")}
</label>
<Controller
name="schedule"
render={({ field }) => (
<AvailabilitySelect
value={field.value}
onBlur={field.onBlur}
name={field.name}
onChange={(selected) => {
field.onChange(selected?.value || null);
}}
/>
)}
/>
</div>
<EventTypeScheduleDetails />
</div>
);
};
const UseCommonScheduleSettingsToggle = () => {
const { t } = useLocale();
const { resetField, setValue } = useFormContext<FormValues>();
return (
<Controller
name="metadata.config.useHostSchedulesForTeamEvent"
@@ -201,6 +206,11 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
checked={!value}
onCheckedChange={(checked) => {
onChange(!checked);
if (checked) {
resetField("schedule");
} else {
setValue("schedule", null);
}
}}
title={t("choose_common_schedule_team_event")}
description={t("choose_common_schedule_team_event_description")}>
@@ -210,3 +220,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
/>
);
};
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />;
};
@@ -74,7 +74,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const removeCustom = (index: number) => {
formMethods.getValues("customInputs").splice(index, 1);
@@ -1,13 +1,13 @@
import { EventTypeSetupProps } from "pages/event-types/[type]";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import RecurringEventController from "./RecurringEventController";
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
const stripeAppData = getStripeAppData(eventType);
const paymentAppData = getPaymentAppData(eventType);
const requirePayment = stripeAppData.price > 0;
const requirePayment = paymentAppData.price > 0;
return (
<div className="">
@@ -196,6 +196,12 @@ export const EventSetupTab = (
if (!eventLocationType) {
return null;
}
// We dont want to translate the string link - it doesnt exist in common.json and it gets prefixed/suffixed with __ or //
const eventLabel =
eventLocationType.defaultValueVariable === "link"
? eventLocationType.label
: t(location[eventLocationType.defaultValueVariable] || eventLocationType.label);
return (
<li
key={`${location.type}${index}`}
@@ -207,9 +213,7 @@ export const EventSetupTab = (
className="h-4 w-4"
alt={`${eventLocationType.label} logo`}
/>
<span className="truncate text-sm ltr:ml-1 rtl:mr-1">
{t(location[eventLocationType.defaultValueVariable] || eventLocationType.label)}
</span>
<span className="truncate text-sm ltr:ml-1 rtl:mr-1">{eventLabel}</span>
</div>
<div className="flex">
<button
+13 -5
View File
@@ -25,6 +25,16 @@ const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
email,
});
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
if (a.label < b.label) {
return -1;
}
if (a.label > b.label) {
return 1;
}
return 0;
};
const FixedHosts = ({
control,
labelText,
@@ -103,7 +113,6 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
];
const teamMembersOptions = teamMembers.map(mapUserToValue);
return (
<div>
{team && (
@@ -128,17 +137,16 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
{schedulingType === SchedulingType.COLLECTIVE && (
<FixedHosts
options={teamMembersOptions}
options={teamMembersOptions.sort(sortByLabel)}
placeholder={t("add_attendees")}
labelText={t("team")}
control={formMethods.control}
/>
)}
{schedulingType === SchedulingType.ROUND_ROBIN && (
<>
<FixedHosts
options={teamMembersOptions}
options={teamMembersOptions.sort(sortByLabel)}
isOptionDisabled={(option) =>
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
}
@@ -173,7 +181,7 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={teamMembers.map(mapUserToValue)}
options={teamMembersOptions.sort(sortByLabel)}
isOptionDisabled={(option) =>
!!formMethods
.getValues("hostsFixed")
@@ -100,7 +100,7 @@ export const EventTeamWebhooksTab = ({
<>
{webhooks.length ? (
<>
<div className="mt-4 mb-8 rounded-md border">
<div className="mb-8 rounded-md border">
{webhooks.map((webhook, index) => {
return (
<WebhookListItem
@@ -286,6 +286,7 @@ function EventTypeSingleLayout({
<DropdownMenuItem className="focus:ring-gray-100">
<DropdownItem
type="button"
color="destructive"
StartIcon={FiTrash}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}>
@@ -338,7 +339,7 @@ function EventTypeSingleLayout({
<div
className={classNames(
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6"
disableBorder ? "border-0 " : "p-2 md:border md:p-6"
)}>
{children}
</div>
@@ -5,6 +5,7 @@ import { FormEvent, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import turndown from "@calcom/lib/turndownService";
import { trpc } from "@calcom/trpc/react";
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
@@ -34,6 +35,7 @@ const UserProfile = (props: IUserProfileProps) => {
const utils = trpc.useContext();
const router = useRouter();
const createEventType = trpc.viewer.eventTypes.create.useMutation();
const telemetry = useTelemetry();
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async (_data, context) => {
@@ -64,6 +66,8 @@ const UserProfile = (props: IUserProfileProps) => {
const onSubmit = handleSubmit((data: { bio: string }) => {
const { bio } = data;
telemetry.event(telemetryEventTypes.onboardingFinished);
mutation.mutate({
bio,
completedOnboarding: true,
@@ -1,9 +1,10 @@
import { ArrowRightIcon } from "@heroicons/react/outline";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { trpc } from "@calcom/trpc/react";
import { Button, TimezoneSelect } from "@calcom/ui";
@@ -20,7 +21,7 @@ const UserSettings = (props: IUserSettingsProps) => {
const { user, nextStep } = props;
const { t } = useLocale();
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
const telemetry = useTelemetry();
const {
register,
handleSubmit,
@@ -31,6 +32,11 @@ const UserSettings = (props: IUserSettingsProps) => {
},
reValidateMode: "onChange",
});
useEffect(() => {
telemetry.event(telemetryEventTypes.onboardingStarted);
}, [telemetry]);
const defaultOptions = { required: true, maxLength: 255 };
const utils = trpc.useContext();
@@ -1,9 +1,8 @@
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge, Button } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale";
import DisableTwoFactorModal from "./DisableTwoFactorModal";
import EnableTwoFactorModal from "./EnableTwoFactorModal";
@@ -1,17 +1,39 @@
import { zodResolver } from "@hookform/resolvers/zod";
import classNames from "classnames";
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import React from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import * as z from "zod";
import { isPasswordValid } from "@calcom/lib/auth";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmailField, Label, PasswordField, TextField } from "@calcom/ui";
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
import { FiUserCheck } from "@calcom/ui/components/icon";
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
const router = useRouter();
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
const { t } = useLocale();
if (props.userCount > 0)
return (
<form
id="wizard-step-1"
name="wizard-step-1"
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
props.onSuccess();
}}>
<EmptyScreen
Icon={FiUserCheck}
headline={t("admin_user_created")}
description={t("admin_user_created_description")}
/>
</form>
);
return <AdminUser {...props} />;
};
export const AdminUser = (props: { onSubmit: () => void; onError: () => void; onSuccess: () => void }) => {
const { t } = useLocale();
const formSchema = z.object({
@@ -45,11 +67,11 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
});
const onError = () => {
props.setIsLoading(false);
props.onError();
};
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
props.setIsLoading(true);
props.onSubmit();
const response = await fetch("/api/auth/setup", {
method: "POST",
body: JSON.stringify({
@@ -69,9 +91,9 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
email: data.email_address.toLowerCase(),
password: data.password,
});
router.replace(`/auth/setup?step=2&category=calendar`);
props.onSuccess();
} else {
router.replace("/auth/setup");
props.onError();
}
}, onError);
@@ -186,5 +208,3 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
</FormProvider>
);
};
export default SetupFormStep1;
@@ -0,0 +1,76 @@
import * as RadioGroup from "@radix-ui/react-radio-group";
import classNames from "classnames";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
const ENTERPRISE_BOOKING_FEE = "$99"; // TODO: get this from a new API endpoint
const ChooseLicense = (
props: {
value: string;
onChange: (value: string) => void;
onSubmit: (value: string) => void;
} & Omit<JSX.IntrinsicElements["form"], "onSubmit" | "onChange">
) => {
const { value: initialValue = "FREE", onChange, onSubmit, ...rest } = props;
const [value, setValue] = useState(initialValue);
const { t } = useLocale();
return (
<form
{...rest}
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
onSubmit(value);
}}>
<RadioGroup.Root
defaultValue={initialValue}
value={value}
aria-label={t("choose_a_license")}
className="grid grid-rows-2 gap-4 md:grid-cols-2 md:grid-rows-1"
onValueChange={(value) => {
onChange(value);
setValue(value);
}}>
<RadioGroup.Item value="FREE">
<div
className={classNames(
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
value === "FREE" && "ring-2 ring-black"
)}>
<h2 className="font-cal text-xl text-black">{t("agplv3_license")}</h2>
<p className="font-medium text-green-800">{t("free_license_fee")}</p>
<p className="text-gray-500">{t("forever_open_and_free")}</p>
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
<li>{t("required_to_keep_your_code_open_source")}</li>
<li>{t("cannot_repackage_and_resell")}</li>
<li>{t("no_enterprise_features")}</li>
</ul>
</div>
</RadioGroup.Item>
<RadioGroup.Item value="EE">
<div
className={classNames(
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
value === "EE" && "ring-2 ring-black"
)}>
<h2 className="font-cal text-xl text-black">{t("ee_enterprise_license")}</h2>
<p className="font-medium text-green-800">
{t("enterprise_booking_fee", { enterprise_booking_fee: ENTERPRISE_BOOKING_FEE })}
</p>
<p className="text-gray-500">{t("enterprise_license_includes")}</p>
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
<li>{t("no_need_to_keep_your_code_open_source")}</li>
<li>{t("repackage_rebrand_resell")}</li>
<li>{t("a_vast_suite_of_enterprise_features")}</li>
</ul>
</div>
</RadioGroup.Item>
</RadioGroup.Root>
</form>
);
};
export default ChooseLicense;
@@ -0,0 +1,144 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { noop } from "lodash";
import { useCallback, useState } from "react";
import { Controller, FormProvider, useForm, useFormState } from "react-hook-form";
import * as z from "zod";
import { classNames } from "@calcom/lib";
import { CONSOLE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
import { Button, TextField } from "@calcom/ui";
import { FiCheck, FiExternalLink, FiLoader } from "@calcom/ui/components/icon";
type EnterpriseLicenseFormValues = {
licenseKey: string;
};
const makeSchemaLicenseKey = (args: { callback: (valid: boolean) => void; onSuccessValidate: () => void }) =>
z.object({
licenseKey: z
.string()
.uuid({
message: "License key must follow UUID format: 8-4-4-4-12",
})
.superRefine(async (data, ctx) => {
const parse = z.string().uuid().safeParse(data);
if (parse.success) {
args.callback(true);
const response = await fetch(`${CONSOLE_URL}/api/license?key=${data}`);
args.callback(false);
const json = await response.json();
if (!json.valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `License key ${json.message.toLowerCase()}`,
});
} else {
args.onSuccessValidate();
}
}
}),
});
const EnterpriseLicense = (
props: {
licenseKey?: string;
initialValue?: Partial<EnterpriseLicenseFormValues>;
onSuccessValidate: () => void;
onSubmit: (value: EnterpriseLicenseFormValues) => void;
onSuccess?: (
data: RouterOutputs["viewer"]["deploymentSetup"]["update"],
variables: RouterInputs["viewer"]["deploymentSetup"]["update"]
) => void;
} & Omit<JSX.IntrinsicElements["form"], "onSubmit">
) => {
const { onSubmit, onSuccess = noop, onSuccessValidate = noop, ...rest } = props;
const { t } = useLocale();
const [checkLicenseLoading, setCheckLicenseLoading] = useState(false);
const mutation = trpc.viewer.deploymentSetup.update.useMutation({
onSuccess,
});
const schemaLicenseKey = useCallback(
() =>
makeSchemaLicenseKey({
callback: setCheckLicenseLoading,
onSuccessValidate,
}),
[setCheckLicenseLoading, onSuccessValidate]
);
const formMethods = useForm<EnterpriseLicenseFormValues>({
defaultValues: {
licenseKey: props.licenseKey || "",
},
resolver: zodResolver(schemaLicenseKey()),
});
const handleSubmit = formMethods.handleSubmit((values) => {
onSubmit(values);
setCheckLicenseLoading(false);
mutation.mutate(values);
});
const { isDirty, errors } = useFormState(formMethods);
return (
<FormProvider {...formMethods}>
<form {...rest} className="space-y-4 rounded-md bg-white px-8 py-10" onSubmit={handleSubmit}>
<div>
<Button
className="w-full justify-center text-lg"
EndIcon={FiExternalLink}
href="https://console.cal.com"
target="_blank">
{t("purchase_license")}
</Button>
<div className="relative flex justify-center">
<hr className="my-8 w-full border-[1.5px] border-gray-200" />
<span className="absolute mt-[22px] bg-white px-3.5 text-sm">OR</span>
</div>
{t("already_have_key")}
<Controller
name="licenseKey"
control={formMethods.control}
render={({ field: { onBlur, onChange, value } }) => (
<TextField
{...formMethods.register("licenseKey")}
className={classNames(
"mb-0 group-hover:border-gray-400",
(checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0"
)}
placeholder="xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
labelSrOnly={true}
value={value}
addOnFilled={false}
addOnClassname={classNames(
"hover:border-gray-300",
errors.licenseKey === undefined && isDirty && "group-hover:border-gray-400"
)}
addOnSuffix={
checkLicenseLoading ? (
<FiLoader className="h-5 w-5 animate-spin" />
) : errors.licenseKey === undefined && isDirty ? (
<FiCheck className="h-5 w-5 text-green-700" />
) : undefined
}
color={errors.licenseKey ? "warn" : ""}
onBlur={onBlur}
onChange={async (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
formMethods.setValue("licenseKey", e.target.value);
await formMethods.trigger("licenseKey");
}}
/>
)}
/>
</div>
</form>
</FormProvider>
);
};
export default EnterpriseLicense;
@@ -1,20 +1,26 @@
import { useRouter } from "next/router";
import { Dispatch, SetStateAction } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { FiCheck } from "@calcom/ui/components/icon";
const StepDone = () => {
const StepDone = (props: {
currentStep: number;
nextStepPath: string;
setIsLoading: Dispatch<SetStateAction<boolean>>;
}) => {
const router = useRouter();
const { t } = useLocale();
return (
<form
id="wizard-step-1"
name="wizard-step-1"
className="space-y-4"
id={`wizard-step-${props.currentStep}`}
name={`wizard-step-${props.currentStep}`}
className="flex justify-center space-y-4"
onSubmit={(e) => {
props.setIsLoading(true);
e.preventDefault();
router.replace(`/auth/setup?step=2&category=calendar`);
router.replace(props.nextStepPath);
}}>
<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">
+2 -3
View File
@@ -3,10 +3,9 @@ import Link from "next/link";
import { TeamPageProps } from "pages/team/[slug]";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Avatar } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale";
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
type TeamType = TeamPageProps["team"];
@@ -20,7 +19,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
return (
<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 w-[90%] 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 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 ">
<Avatar
size="md"
alt={member.name || ""}
+1 -1
View File
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
<label className="min-w-8 relative inline-block">
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value}&nbsp;</span>
{!isEditing && isReady && (
<FiEdit2 className="ml-1 inline h-4 w-4 align-top text-gray-700 group-hover:text-gray-500" />
<FiEdit2 className=" ml-1 -mt-px inline h-3 w-3 text-gray-500 group-hover:text-gray-500" />
)}
<input
{...passThroughProps}
@@ -1,4 +1,3 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
@@ -6,6 +5,8 @@ import { User } from "@calcom/prisma/client";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { AppRouter } from "@calcom/trpc/server/routers/_app";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { PremiumTextfield } from "./PremiumTextfield";
import { UsernameTextfield } from "./UsernameTextfield";
@@ -21,7 +22,7 @@ export const UsernameAvailabilityField = ({
onErrorMutation,
user,
}: UsernameAvailabilityFieldProps) => {
const [currentUsername, setCurrentUsername] = useState(user.username ?? "");
const { username: currentUsername, setQuery: setCurrentUsername } = useRouterQuery("username");
const formMethods = useForm({
defaultValues: {
username: currentUsername,
+1 -1
View File
@@ -88,7 +88,7 @@ const withQuery = <
TInput = inferProcedureInput<TQuery>,
TOutput = inferProcedureOutput<TQuery>
>(
queryProcedure: DecorateProcedure<TQuery, any>,
queryProcedure: DecorateProcedure<TQuery, any, any>,
input?: TInput,
params?: UseTRPCQueryOptions<any, TInput, TOutput, TOutput, TError>
) => {
+4 -1
View File
@@ -13,18 +13,20 @@ import { trpc } from "@calcom/trpc/react";
import { MetaProvider } from "@calcom/ui";
import usePublicPage from "@lib/hooks/usePublicPage";
import { WithNonceProps } from "@lib/withNonce";
const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children: React.ReactNode }>(
({ children }) => <>{children}</>
);
// Workaround for https://github.com/vercel/next.js/issues/8592
export type AppProps = Omit<NextAppProps, "Component"> & {
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
Component: NextAppProps["Component"] & {
requiresLicense?: boolean;
isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean);
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
};
/** Will be defined only is there was an error */
err?: Error;
};
@@ -77,6 +79,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
<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 */}
<ThemeProvider
nonce={props.pageProps.nonce}
enableColorScheme={false}
storageKey={storageKey}
forcedTheme={forcedTheme}
+83
View File
@@ -0,0 +1,83 @@
import crypto from "crypto";
import { IncomingMessage, OutgoingMessage } from "http";
import { z } from "zod";
import { IS_PRODUCTION } from "@calcom/lib/constants";
function getCspPolicy(nonce: string) {
//TODO: Do we need to explicitly define it in turbo.json
const CSP_POLICY = process.env.CSP_POLICY;
// Note: "non-strict" policy only allows inline styles otherwise it's the same as "strict"
// 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.
const useNonStrictPolicy = CSP_POLICY === "non-strict";
return `
default-src 'self' ${IS_PRODUCTION ? "" : "data:"};
script-src ${
IS_PRODUCTION
? // 'self' 'unsafe-inline' https: added for Browsers not supporting strict-dynamic not supporting strict-dynamic
"'nonce-" + nonce + "' 'strict-dynamic' 'self' 'unsafe-inline' https:"
: // Note: We could use 'strict-dynamic' with 'nonce-..' instead of unsafe-inline but there are some streaming related scripts that get blocked(because they don't have nonce on them). It causes a really frustrating full page error model by Next.js to show up sometimes
"'unsafe-inline' 'unsafe-eval' https: http:"
};
object-src 'none';
base-uri 'none';
child-src app.cal.com;
style-src 'self' ${
IS_PRODUCTION ? (useNonStrictPolicy ? "'unsafe-inline'" : "") : "'unsafe-inline'"
} app.cal.com;
font-src 'self';
img-src 'self' https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:
`;
}
// Taken from @next-safe/middleware
const isPagePathRequest = (url: URL) => {
const isNonPagePathPrefix = /^\/(?:_next|api)\//;
const isFile = /\..*$/;
const { pathname } = url;
return !isNonPagePathPrefix.test(pathname) && !isFile.test(pathname);
};
export function csp(req: IncomingMessage | null, res: OutgoingMessage | null) {
if (!req) {
return { nonce: undefined };
}
const existingNonce = req.headers["x-nonce"];
if (existingNonce) {
const existingNoneParsed = z.string().safeParse(existingNonce);
return { nonce: existingNoneParsed.success ? existingNoneParsed.data : "" };
}
if (!req.url) {
return { nonce: undefined };
}
const CSP_POLICY = process.env.CSP_POLICY;
const cspEnabledForInstance = CSP_POLICY;
const nonce = crypto.randomBytes(16).toString("base64");
const parsedUrl = new URL(req.url, "http://base_url");
const cspEnabledForPage = cspEnabledForInstance && isPagePathRequest(parsedUrl);
if (!cspEnabledForPage) {
return {
nonce: undefined,
};
}
// Set x-nonce request header to be used by `getServerSideProps` or similar fns and `Document.getInitialProps` to read the nonce from
// It is generated for all page requests but only used by pages that need CSP
req.headers["x-nonce"] = nonce;
if (res) {
res.setHeader(
req.headers["x-csp-enforce"] === "true"
? "Content-Security-Policy"
: "Content-Security-Policy-Report-Only",
getCspPolicy(nonce)
.replace(/\s{2,}/g, " ")
.trim()
);
}
return { nonce };
}
-11
View File
@@ -1,11 +0,0 @@
import { useTranslation } from "next-i18next";
/** @deprecated use the one from `@calcom/lib/hooks/useLocale` */
export const useLocale = () => {
const { i18n, t } = useTranslation("common");
return {
i18n,
t,
};
};
+41
View File
@@ -0,0 +1,41 @@
import { GetServerSideProps, GetServerSidePropsContext } from "next";
import { csp } from "@lib/csp";
export type WithNonceProps = {
nonce?: string;
};
/**
* Make any getServerSideProps fn return the nonce so that it can be used by Components in the page to add any script tag.
* Note that if the Components are not adding any script tag then this is not needed. Even in absence of this, Document.getInitialProps would be able to generate nonce itself which it needs to add script tags common to all pages
* There is no harm in wrapping a `getServerSideProps` fn with this even if it doesn't add any script tag.
*/
export default function withNonce(getServerSideProps: GetServerSideProps) {
return async (context: GetServerSidePropsContext) => {
const ssrResponse = await getServerSideProps(context);
const { nonce } = csp(context.req, context.res);
// Skip nonce property if it's not available instead of setting it to undefined because undefined can't be serialized.
const nonceProps = nonce
? {
nonce,
}
: null;
if (!("props" in ssrResponse)) {
return ssrResponse;
}
// Helps in debugging that withNonce was used but a valid nonce couldn't be set
context.res.setHeader("x-csp", nonce ? "ssr" : "false");
return {
...ssrResponse,
props: {
...ssrResponse.props,
...nonceProps,
},
};
};
}
+52 -1
View File
@@ -1,3 +1,4 @@
import { get } from "@vercel/edge-config";
import { collectEvents } from "next-collect/server";
import { NextMiddleware, NextResponse, userAgent } from "next/server";
@@ -8,6 +9,28 @@ import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry
const middleware: NextMiddleware = async (req) => {
const url = req.nextUrl;
if (!url.pathname.startsWith("/api")) {
//
// NOTE: When tRPC hits an error a 500 is returned, when this is received
// by the application the user is automatically redirected to /auth/login.
//
// - For this reason our matchers are sufficient for an app-wide maintenance page.
//
try {
// Check whether the maintenance page should be shown
const isInMaintenanceMode = await get<boolean>("isInMaintenanceMode");
// If is in maintenance mode, point the url pathname to the maintenance page
if (isInMaintenanceMode) {
req.nextUrl.pathname = `/maintenance`;
return NextResponse.rewrite(req.nextUrl);
}
} catch (error) {
// show the default page if EDGE_CONFIG env var is missing,
// but log the error to the console
// console.error(error);
}
}
if (["/api/collect-events", "/api/auth"].some((p) => url.pathname.startsWith(p))) {
const callbackUrl = url.searchParams.get("callbackUrl");
const { isBot } = userAgent(req);
@@ -36,11 +59,39 @@ const middleware: NextMiddleware = async (req) => {
return NextResponse.rewrite(url);
}
if (url.pathname.startsWith("/api/trpc/")) {
const requestHeaders = new Headers(req.headers);
requestHeaders.set("x-cal-timezone", req.headers.get("x-vercel-ip-timezone") ?? "");
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
if (url.pathname.startsWith("/auth/login")) {
const moreHeaders = new Headers(req.headers);
// Use this header to actually enforce CSP, otherwise it is running in Report Only mode on all pages.
moreHeaders.set("x-csp-enforce", "true");
return NextResponse.next({
request: {
headers: moreHeaders,
},
});
}
return NextResponse.next();
};
export const config = {
matcher: ["/api/collect-events/:path*", "/api/auth/:path*", "/apps/routing_forms/:path*", "/:path*/embed"],
matcher: [
"/api/collect-events/:path*",
"/api/auth/:path*",
"/apps/routing_forms/:path*",
"/:path*/embed",
"/api/trpc/:path*",
"/auth/login",
],
};
export default collectEvents({
+32 -4
View File
@@ -35,6 +35,12 @@ if (!process.env.NEXT_PUBLIC_WEBSITE_URL) {
process.env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL;
}
if (process.env.CSP_POLICY === "strict" && process.env.NODE_ENV === "production") {
throw new Error(
"Strict CSP policy(for style-src) is not yet supported in production. You can experiment with it in Dev Mode"
);
}
if (!process.env.EMAIL_FROM) {
console.warn(
"\x1b[33mwarn",
@@ -76,7 +82,6 @@ if (process.env.ANALYZE === "true") {
plugins.push(withTM);
plugins.push(withAxiom);
/** @type {import("next").NextConfig} */
const nextConfig = {
i18n,
@@ -98,7 +103,7 @@ const nextConfig = {
images: {
unoptimized: true,
},
webpack: (config) => {
webpack: (config, { webpack, buildId }) => {
config.plugins.push(
new CopyWebpackPlugin({
patterns: [
@@ -120,6 +125,8 @@ const nextConfig = {
})
);
config.plugins.push(new webpack.DefinePlugin({ "process.env.BUILD_ID": JSON.stringify(buildId) }));
config.resolve.fallback = {
...config.resolve.fallback, // if you miss it, all the other options in fallback, specified
// by next.js will be dropped. Doesn't make much sense, but how it is
@@ -179,8 +186,7 @@ const nextConfig = {
async headers() {
return [
{
// prettier-ignore
source: "/:path*((?<!\/embed$)(?<!\/embed\/preview\.html$))",
source: "/auth/:path*",
headers: [
{
key: "X-Frame-Options",
@@ -188,6 +194,28 @@ const nextConfig = {
},
],
},
{
source: "/signup",
headers: [
{
key: "X-Frame-Options",
value: "DENY",
},
],
},
{
source: "/:path*",
headers: [
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
],
},
];
},
async redirects() {
+8 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.5.8",
"version": "2.6.0",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -61,6 +61,7 @@
"@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9",
"@types/turndown": "^5.0.1",
"@vercel/edge-config": "^0.1.1",
"@vercel/edge-functions-ui": "^0.2.1",
"@vercel/og": "^0.0.21",
"accept-language-parser": "^1.5.0",
@@ -165,5 +166,11 @@
"ts-jest": "^28.0.8",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
},
"nextBundleAnalysis": {
"budget": 358400,
"budgetPercentIncreaseRed": 20,
"minimumChangeThreshold": 500,
"showDetails": true
}
}
+1 -2
View File
@@ -4,11 +4,10 @@ import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui";
import { FiFileText, FiCheck, FiBookOpen, FiChevronRight } from "@calcom/ui/components/icon";
import { useLocale } from "@lib/hooks/useLocale";
import { ssgInit } from "@server/lib/ssg";
export default function Custom404() {
+12 -3
View File
@@ -60,7 +60,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
{eventTypes.map((type, index) => (
<li
key={index}
className="dark:bg-darkgray-100 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 dark:border-gray-700 dark:hover:border-gray-600">
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">
<FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
<Link
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
@@ -156,7 +156,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<div
className={classNames(
"rounded-md ",
!isEventListEmpty && "border border-gray-200 dark:border-gray-700 dark:hover:border-gray-600"
!isEventListEmpty && "dark:border-darkgray-300 border border-gray-200"
)}
data-testid="event-types">
{user.away ? (
@@ -175,7 +175,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<div
key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
className="dark:bg-darkgray-100 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 dark:border-gray-700 dark:hover:border-gray-600">
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">
<FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<Link
@@ -291,6 +291,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}
const isDynamicGroup = users.length > 1;
if (isDynamicGroup) {
// sort and be in the same order as usernameList so first user is the first user in the list
users.sort((a, b) => {
const aIndex = (a.username && usernameList.indexOf(a.username)) || 0;
const bIndex = (b.username && usernameList.indexOf(b.username)) || 0;
return aIndex - bIndex;
});
}
const dynamicNames = isDynamicGroup
? users.map((user) => {
return user.name || "";
+33 -2
View File
@@ -3,13 +3,18 @@ import { GetStaticPaths, GetStaticPropsContext } from "next";
import { z } from "zod";
import { privacyFilteredLocations, LocationObject } from "@calcom/app-store/locations";
import { getAppFromSlug } from "@calcom/app-store/utils";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
import prisma from "@calcom/prisma";
import { User } from "@calcom/prisma/client";
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
import {
EventTypeMetaDataSchema,
teamMetadataSchema,
userMetadata as userMetadataSchema,
} from "@calcom/prisma/zod-utils";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -215,6 +220,7 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
darkBrandColor: true,
defaultScheduleId: true,
allowDynamicBooking: true,
metadata: true,
away: true,
schedules: {
select: {
@@ -233,7 +239,32 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
};
}
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
// sort and be in the same order as usernameList so first user is the first user in the list
let sortedUsers: typeof users = [];
if (users.length > 1) {
sortedUsers = users.sort((a, b) => {
const aIndex = (a.username && usernameList.indexOf(a.username)) || 0;
const bIndex = (b.username && usernameList.indexOf(b.username)) || 0;
return aIndex - bIndex;
});
}
let locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
// Get the prefered location type from the first user
const firstUsersMetadata = userMetadataSchema.parse(sortedUsers[0].metadata || {});
const preferedLocationType = firstUsersMetadata?.defaultConferencingApp;
if (preferedLocationType?.appSlug) {
const foundApp = getAppFromSlug(preferedLocationType.appSlug);
const appType = foundApp?.appData?.location?.type;
if (appType) {
// Replace the location with the prefered location type
// This will still be default to daily if the app is not found
locations = [{ type: appType, link: preferedLocationType.appLink }] as LocationObject[];
}
}
const eventTypeObject = Object.assign({}, eventType, {
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
+38 -2
View File
@@ -1,6 +1,7 @@
import { GetServerSidePropsContext } from "next";
import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations";
import { getAppFromSlug } from "@calcom/app-store/utils";
import { parseRecurringEvent } from "@calcom/lib";
import {
getDefaultEvent,
@@ -11,7 +12,11 @@ import {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { bookEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import {
customInputSchema,
EventTypeMetaDataSchema,
userMetadata as userMetadataSchema,
} from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import getBooking, { GetBookingType } from "@lib/getBooking";
@@ -85,6 +90,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
darkBrandColor: true,
allowDynamicBooking: true,
away: true,
metadata: true,
},
});
@@ -117,8 +123,38 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
};
const eventTypeObject = [eventType].map((e) => {
const getLocations = () => {
let locations = eventTypeRaw.locations || [];
if (!isDynamicGroupBooking) return locations;
let sortedUsers: typeof users = [];
// sort and be in the same order as usernameList so first user is the first user in the list
if (users.length > 1) {
sortedUsers = users.sort((a, b) => {
const aIndex = (a.username && usernameList.indexOf(a.username)) || 0;
const bIndex = (b.username && usernameList.indexOf(b.username)) || 0;
return aIndex - bIndex;
});
}
// Get the prefered location type from the first user
const firstUsersMetadata = userMetadataSchema.parse(sortedUsers[0].metadata || {});
const preferedLocationType = firstUsersMetadata?.defaultConferencingApp;
if (preferedLocationType?.appSlug) {
const foundApp = getAppFromSlug(preferedLocationType.appSlug);
const appType = foundApp?.appData?.location?.type;
if (appType) {
// Replace the location with the prefered location type
// This will still be default to daily if the app is not found
locations = [{ type: appType, link: preferedLocationType.appLink }] as LocationObject[];
}
}
return locations;
};
const eventTypeObject = [eventType].map((e) => {
let locations = getLocations();
locations = privacyFilteredLocations(locations as LocationObject[]);
return {
...e,
@@ -0,0 +1,67 @@
/**
* This page is empty for the user, it is used only to take advantage of the
* caching system that NextJS uses SSG pages.
* TODO: Redirect to user profile on browser
*/
import { GetStaticPaths, GetStaticProps } from "next";
import { z } from "zod";
import { getCachedResults } from "@calcom/core";
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
const CalendarCache = () => <div />;
const paramsSchema = z.object({ user: z.string(), month: z.string() });
export const getStaticProps: GetStaticProps<
{ results: Awaited<ReturnType<typeof getCachedResults>> },
{ user: string }
> = async (context) => {
const { user: username, month } = paramsSchema.parse(context.params);
const user = await prisma.user.findUnique({
where: {
username,
},
select: {
id: true,
username: true,
credentials: true,
selectedCalendars: true,
},
});
const startDate = (
dayjs(month, "YYYY-MM").isSame(dayjs(), "month") ? dayjs() : dayjs(month, "YYYY-MM")
).startOf("day");
const endDate = startDate.endOf("month");
try {
const results = user?.credentials
? await getCachedResults(
user?.credentials,
startDate.format(),
endDate.format(),
user?.selectedCalendars
)
: [];
return {
props: { results, date: new Date().toISOString() },
revalidate: 1,
};
} catch (error) {
let message = "Unknown error while fetching calendarƒ";
if (error instanceof Error) message = error.message;
console.error(error, message);
return {
props: { results: [], date: new Date().toISOString(), message },
revalidate: 1,
};
}
};
export const getStaticPaths: GetStaticPaths = () => {
return {
paths: [],
fallback: "blocking",
};
};
export default CalendarCache;
+17 -86
View File
@@ -1,18 +1,10 @@
import { DefaultSeo } from "next-seo";
import Head from "next/head";
import Script from "next/script";
import superjson from "superjson";
import "@calcom/embed-core/src/embed-iframe";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
import { httpLink } from "@calcom/trpc/client/links/httpLink";
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
import { splitLink } from "@calcom/trpc/client/links/splitLink";
import { withTRPC } from "@calcom/trpc/next";
import type { TRPCClientErrorLike } from "@calcom/trpc/react";
import { Maybe } from "@calcom/trpc/server";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { trpc } from "@calcom/trpc/react";
import AppProviders, { AppProps } from "@lib/app-providers";
import { seoConfig } from "@lib/config/next-seo.config";
@@ -29,13 +21,27 @@ function MyApp(props: AppProps) {
} else if (router.pathname === "/500") {
pageStatus = "500";
}
// On client side don't let nonce creep into DOM
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
// See https://github.com/kentcdodds/nonce-hydration-issues
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce;
const providerProps = {
...props,
pageProps: {
...props.pageProps,
nonce,
},
};
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page);
return (
<AppProviders {...props}>
<AppProviders {...providerProps}>
<DefaultSeo {...seoConfig.defaultNextSeo} />
<I18nLanguageHandler />
<Script
nonce={nonce}
id="page-status"
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
/>
@@ -56,79 +62,4 @@ function MyApp(props: AppProps) {
);
}
export default withTRPC<AppRouter>({
config() {
const url =
typeof window !== "undefined"
? "/api/trpc"
: process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: `http://${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
/**
* If you want to use SSR, you need to use the server's full URL
* @link https://trpc.io/docs/ssr
*/
return {
/**
* @link https://trpc.io/docs/links
*/
links: [
// adds pretty logs to your console in development and logs errors in production
loggerLink({
enabled: (opts) =>
!!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error),
}),
splitLink({
// check for context property `skipBatch`
condition: (op) => {
return op.context.skipBatch === true;
},
// when condition is true, use normal request
true: httpLink({ url }),
// when condition is false, use batching
false: httpBatchLink({
url,
/** @link https://github.com/trpc/trpc/issues/2008 */
// maxBatchSize: 7
}),
}),
],
/**
* @link https://react-query.tanstack.com/reference/QueryClient
*/
queryClientConfig: {
defaultOptions: {
queries: {
/**
* 1s should be enough to just keep identical query waterfalls low
* @example if one page components uses a query that is also used further down the tree
*/
staleTime: 1000,
/**
* Retry `useQuery()` calls depending on this function
*/
retry(failureCount, _err) {
const err = _err as never as Maybe<TRPCClientErrorLike<AppRouter>>;
const code = err?.data?.code;
if (code === "BAD_REQUEST" || code === "FORBIDDEN" || code === "UNAUTHORIZED") {
// if input data is wrong or you're not authorized there's no point retrying a query
return false;
}
const MAX_QUERY_RETRIES = 3;
return failureCount < MAX_QUERY_RETRIES;
},
},
},
},
/**
* @link https://trpc.io/docs/data-transformers
*/
transformer: superjson,
};
},
/**
* @link https://trpc.io/docs/ssr
*/
ssr: false,
})(MyApp);
export default trpc.withTRPC(MyApp);
+18 -49
View File
@@ -1,58 +1,36 @@
import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps } from "next/document";
import Script from "next/script";
import { z } from "zod";
import { getDirFromLang } from "@calcom/lib/i18n";
import { csp } from "@lib/csp";
type Props = Record<string, unknown> & DocumentProps;
function toRunBeforeReactOnClient() {
const calEmbedMode = typeof new URL(document.URL).searchParams.get("embed") === "string";
/* Iframe Name */
window.name.includes("cal-embed");
window.isEmbed = () => {
// Once an embed mode always an embed mode
return calEmbedMode;
};
window.resetEmbedStatus = () => {
try {
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
window.sessionStorage.removeItem("calEmbedMode");
} catch (e) {}
};
window.getEmbedTheme = () => {
const url = new URL(document.URL);
return url.searchParams.get("theme") as "dark" | "light";
};
window.getEmbedNamespace = () => {
const url = new URL(document.URL);
const namespace = url.searchParams.get("embed");
return namespace;
};
window.isPageOptimizedForEmbed = () => {
// Those pages are considered optimized, which know at backend that they are rendering for embed.
// Such pages can be shown straightaway without a loader for a better embed experience
return location.pathname.includes("forms/");
};
}
class MyDocument extends Document<Props> {
static async getInitialProps(ctx: DocumentContext) {
const { nonce } = csp(ctx.req || null, ctx.res || null);
if (!process.env.CSP_POLICY) {
ctx.res?.setHeader("x-csp", "not-opted-in");
} else if (!ctx.res?.getHeader("x-csp")) {
// If x-csp not set by gSSP, then it's initialPropsOnly
ctx.res?.setHeader("x-csp", "initialPropsOnly");
}
const isEmbed = ctx.asPath?.includes("/embed") || ctx.asPath?.includes("embedType=");
const initialProps = await Document.getInitialProps(ctx);
return { isEmbed, ...initialProps };
return { isEmbed, nonce, ...initialProps };
}
render() {
const { locale } = this.props.__NEXT_DATA__;
const { isEmbed } = this.props;
const nonceParsed = z.string().safeParse(this.props.nonce);
const nonce = nonceParsed.success ? nonceParsed.data : "";
const dir = getDirFromLang(locale);
return (
<Html lang={locale} dir={dir}>
<Head>
<Head nonce={nonce}>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
@@ -68,22 +46,13 @@ class MyDocument extends Document<Props> {
crossOrigin="anonymous"
/>
<link rel="preload" href="/fonts/cal.ttf" as="font" type="font/ttf" crossOrigin="anonymous" />
{/* Define isEmbed here so that it can be shared with App(embed-iframe) as well as the following code to change background and hide body
Persist the embed mode in sessionStorage because query param might get lost during browsing.
*/}
<Script
id="run-before-client"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `(${toRunBeforeReactOnClient.toString()})()`,
}}
/>
<Script src="/embed-init-iframe.js" strategy="beforeInteractive" />
</Head>
<body
className="dark:bg-darkgray-50 desktop-transparent bg-gray-100 antialiased"
style={
this.props.isEmbed
isEmbed
? {
background: "transparent",
// Keep the embed hidden till parent initializes and
@@ -95,7 +64,7 @@ class MyDocument extends Document<Props> {
: {}
}>
<Main />
<NextScript />
<NextScript nonce={nonce} />
</body>
</Html>
);
+10 -2
View File
@@ -164,6 +164,7 @@ if (IS_GOOGLE_LOGIN_ENABLED) {
GoogleProvider({
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
allowDangerousEmailAccountLinking: true,
})
);
}
@@ -202,6 +203,7 @@ if (isSAMLLoginEnabled) {
clientId: "dummy",
clientSecret: "dummy",
},
allowDangerousEmailAccountLinking: true,
});
}
@@ -360,7 +362,7 @@ export default NextAuth({
return token;
},
async session({ session, token }) {
const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || "");
const hasValidLicense = await checkLicense(prisma);
const calendsoSession: Session = {
...session,
hasValidLicense,
@@ -497,7 +499,13 @@ export default NextAuth({
return true;
}
if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
// User signs up with email/password and then tries to login with Google/SAML using the same email
if (
existingUserWithEmail.identityProvider === IdentityProvider.CAL &&
(idP === IdentityProvider.GOOGLE || idP === IdentityProvider.SAML)
) {
return true;
} else if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
return "/auth/error?error=use-password-login";
}
+1 -1
View File
@@ -43,7 +43,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
OR: [
{ username },
{
AND: [{ email: userEmail }, { password: { not: null } }, { username: { not: null } }],
AND: [{ email: userEmail }],
},
],
},
@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import notEmpty from "@calcom/lib/notEmpty";
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
@@ -65,6 +66,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.status(200).json({ message: "Calendar Selection Saved" });
}
if (["DELETE", "POST"].includes(req.method)) {
await revalidateCalendarCache(res.revalidate, `${session?.user?.username}`);
}
if (req.method === "GET") {
const selectedCalendarIds = await prisma.selectedCalendar.findMany({
where: {
+7 -1
View File
@@ -4,6 +4,7 @@ import type { Session } from "next-auth";
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
import { getSession } from "@calcom/lib/auth";
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
import prisma from "@calcom/prisma";
import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler";
@@ -53,7 +54,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
/* Absolute path didn't work */
const handlerMap = (await import("@calcom/app-store/apps.server.generated")).apiHandlers;
const handlerKey = deriveAppDictKeyFromType(appName, handlerMap);
const handlers = await handlerMap[handlerKey as keyof typeof handlerMap];
const handler = handlers[apiEndpoint as keyof typeof handlers] as AppHandler;
@@ -63,8 +63,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (typeof handler === "function") {
await handler(req, res);
if (appName.includes("calendar") && req.session?.user?.username) {
await revalidateCalendarCache(res.revalidate, req.session?.user?.username);
}
} else {
await defaultIntegrationAddHandler({ user: req.session?.user, ...handler });
if (handler.appType.includes("calendar") && req.session?.user?.username) {
await revalidateCalendarCache(res.revalidate, req.session?.user?.username);
}
redirectUrl = handler.redirect?.url || getInstalledAppPath(handler);
res.json({ url: redirectUrl, newTab: handler.redirect?.newTab });
}
+33
View File
@@ -0,0 +1,33 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import { performance } from "@calcom/lib/server/perfObserver";
let isCold = true;
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
const prePrismaDate = performance.now();
const prisma = (await import("@calcom/prisma")).default;
const preSessionDate = performance.now();
const session = await getSession({ req });
if (!session) return res.status(409).json({ message: "Unauthorized" });
const preUserDate = performance.now();
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
if (!user) return res.status(404).json({ message: "No user found" });
const lastUpdate = performance.now();
res.setHeader("x-is-cold", isCold.toString());
isCold = false;
return res.status(200).json({
message: `Hello ${user.name}`,
prePrismaDate,
prismaDuration: `Prisma took ${preSessionDate - prePrismaDate}ms`,
preSessionDate,
sessionDuration: `Session took ${preUserDate - preSessionDate}ms`,
preUserDate,
userDuration: `User took ${lastUpdate - preUserDate}ms`,
lastUpdate,
wasCold: isCold,
});
}
@@ -0,0 +1,27 @@
import { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
const querySchema = z.object({
username: z.string(),
});
/**
* This endpoint revalidates users calendar cache several months ahead
* Can be used as webhook
* @param req
* @param res
* @returns
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { username } = querySchema.parse(req.query);
try {
await revalidateCalendarCache(res.revalidate, username);
return res.status(200).json({ revalidated: true });
} catch (err) {
return res.status(500).send({ message: "Error revalidating" });
}
}
+36 -22
View File
@@ -1,6 +1,8 @@
/**
* This file contains tRPC's HTTP response handler
*/
import { z } from "zod";
import * as trpcNext from "@calcom/trpc/server/adapters/next";
import { createContext } from "@calcom/trpc/server/createContext";
import { appRouter } from "@calcom/trpc/server/routers/_app";
@@ -30,34 +32,46 @@ export default trpcNext.createNextApiHandler({
* @link https://trpc.io/docs/caching#api-response-caching
*/
responseMeta({ ctx, paths, type, errors }) {
// assuming we have all our public routes in `viewer.public`
// Some helpers relevant to this function only
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
// assuming you have all your public routes with the keyword `public` in them
const allPublic = paths && paths.every((path) => path.startsWith("viewer.public."));
// checking that no procedures errored
const allOk = errors.length === 0;
// checking we're doing a query request
const isQuery = type === "query";
const noHeaders = {};
// i18n response depends on request header
const nonCacheableQueries = ["viewer.public.i18n"];
const isThereANonCacheableQuery = paths?.some((path) => nonCacheableQueries.includes(path));
const isThereACacheableQuery = paths?.some((path) => !nonCacheableQueries.includes(path));
if (isThereANonCacheableQuery && isThereACacheableQuery) {
console.warn(
"Cacheable and Non-cacheable queries are mixed in the same request. Not going to cache the request"
);
}
// We cannot set headers on SSG queries
if (!ctx?.res) return noHeaders;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ctx.req is available for SSR but not SSG
if (!!ctx?.req && allPublic && allOk && isQuery && !isThereANonCacheableQuery) {
// cache request for 1 day + revalidate once every 5 seconds
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: {
"cache-control": `s-maxage=5, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
},
};
}
return {};
const defaultHeaders: Record<"headers", Record<string, string>> = {
headers: {},
};
const timezone = z.string().safeParse(ctx.req?.headers["x-vercel-ip-timezone"]);
if (timezone.success) defaultHeaders.headers["x-cal-timezone"] = timezone.data;
// We need all these conditions to be true to set cache headers
if (!(allPublic && allOk && isQuery)) return defaultHeaders;
// No cache by default
defaultHeaders.headers["cache-control"] = `no-cache`;
// Our cache can change depending on our current paths value. Since paths is an array,
// we want to create a map that can match potential paths with their desired cache value
const cacheRules = {
"viewer.public.session": `no-cache`,
"viewer.public.i18n": `no-cache`,
// Revalidation time here should be 1 second, per https://github.com/calcom/cal.com/pull/6823#issuecomment-1423215321
"viewer.public.slots.getSchedule": `no-cache`, // FIXME
} as const;
// Find which element above is an exact match for this group of paths
const matchedPath = paths.find((v) => v in cacheRules) as keyof typeof cacheRules;
if (matchedPath) defaultHeaders.headers["cache-control"] = cacheRules[matchedPath];
return defaultHeaders;
},
});
+152 -96
View File
@@ -1,10 +1,12 @@
import { useRouter } from "next/router";
import { useReducer } from "react";
import { useReducer, useState } from "react";
import z from "zod";
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
import { InstallAppButton } from "@calcom/app-store/components";
import { EventLocationType, getEventLocationTypeFromApp } from "@calcom/app-store/locations";
import { InstalledAppVariants } from "@calcom/app-store/utils";
import { AppSetDefaultLinkDailog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
@@ -22,6 +24,7 @@ import {
Dropdown,
DropdownMenuItem,
DropdownItem,
showToast,
} from "@calcom/ui";
import {
FiBarChart,
@@ -41,7 +44,7 @@ import AppListCard from "@components/AppListCard";
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
function ConnectOrDisconnectIntegrationButton(props: {
function ConnectOrDisconnectIntegrationMenuItem(props: {
credentialIds: number[];
type: App["type"];
isGlobal?: boolean;
@@ -60,22 +63,15 @@ function ConnectOrDisconnectIntegrationButton(props: {
if (credentialId || type === "stripe_payment" || isGlobal) {
return (
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownItem
color="destructive"
onClick={() => handleDisconnect(credentialId)}
disabled={isGlobal}
StartIcon={FiTrash}>
{t("remove_app")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
<DropdownMenuItem>
<DropdownItem
color="destructive"
onClick={() => handleDisconnect(credentialId)}
disabled={isGlobal}
StartIcon={FiTrash}>
{t("remove_app")}
</DropdownItem>
</DropdownMenuItem>
);
}
@@ -112,37 +108,98 @@ interface IntegrationsListProps {
handleDisconnect: (credentialId: number) => void;
}
const IntegrationsList = ({ data, handleDisconnect }: IntegrationsListProps) => {
const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListProps) => {
const { data: defaultConferencingApp } = trpc.viewer.getUsersDefaultConferencingApp.useQuery();
const utils = trpc.useContext();
const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
undefined
);
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
onSuccess: () => {
showToast("Default app updated successfully", "success");
utils.viewer.getUsersDefaultConferencingApp.invalidate();
},
onError: (error) => {
showToast(`Error: ${error.message}`, "error");
},
});
const { t } = useLocale();
return (
<List>
{data.items
.filter((item) => item.invalidCredentialIds)
.map((item) => (
<AppListCard
key={item.name}
description={item.description}
title={item.name}
logo={item.logo}
isDefault={item.isGlobal}
shouldHighlight
slug={item.slug}
invalidCredential={item.invalidCredentialIds.length > 0}
actions={
<div className="flex justify-end">
<ConnectOrDisconnectIntegrationButton
credentialIds={item.credentialIds}
type={item.type}
isGlobal={item.isGlobal}
installed
invalidCredentialIds={item.invalidCredentialIds}
handleDisconnect={handleDisconnect}
/>
</div>
}>
<AppSettings slug={item.slug} />
</AppListCard>
))}
</List>
<>
<List>
{data.items
.filter((item) => item.invalidCredentialIds)
.map((item) => {
const appSlug = item?.slug;
const appIsDefault =
appSlug === defaultConferencingApp?.appSlug ||
(appSlug === "daily-video" && !defaultConferencingApp?.appSlug);
return (
<AppListCard
key={item.name}
description={item.description}
title={item.name}
logo={item.logo}
isDefault={appIsDefault}
shouldHighlight
slug={item.slug}
invalidCredential={item.invalidCredentialIds.length > 0}
actions={
<div className="flex justify-end">
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{!appIsDefault && variant === "conferencing" && (
<DropdownMenuItem>
<DropdownItem
type="button"
color="secondary"
StartIcon={FiVideo}
onClick={() => {
const locationType = getEventLocationTypeFromApp(
item?.locationOption?.value ?? ""
);
if (locationType?.linkType === "static") {
setLocationType({ ...locationType, slug: appSlug });
} else {
updateDefaultAppMutation.mutate({
appSlug,
});
}
}}>
{t("change_default_conferencing_app")}
</DropdownItem>
</DropdownMenuItem>
)}
<ConnectOrDisconnectIntegrationMenuItem
credentialIds={item.credentialIds}
type={item.type}
isGlobal={item.isGlobal}
installed
invalidCredentialIds={item.invalidCredentialIds}
handleDisconnect={handleDisconnect}
/>
</DropdownMenuContent>
</Dropdown>
</div>
}>
<AppSettings slug={item.slug} />
</AppListCard>
);
})}
</List>
{locationType && (
<AppSetDefaultLinkDailog
locationType={locationType}
setLocationType={() => setLocationType(undefined)}
/>
)}
</>
);
};
@@ -168,46 +225,43 @@ const IntegrationsContainer = ({
query={query}
customLoader={<SkeletonLoader />}
success={({ data }) => {
if (!data.items.length) {
return (
<EmptyScreen
Icon={emptyIcon[variant || "other"]}
headline={t("no_category_apps", {
category: (variant && t(variant).toLowerCase()) || t("other").toLowerCase(),
})}
description={t(`no_category_apps_description_${variant || "other"}`)}
buttonRaw={
<Button
color="secondary"
data-testid={`connect-${variant || "other"}-apps`}
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
{t(`connect_${variant || "other"}_apps`)}
</Button>
}
/>
);
}
return (
<>
{data.items.length > 0 ? (
<div className="rounded-md border border-gray-200 p-7">
<ShellSubHeading
title={t(variant || "other")}
subtitle={t(`installed_app_${variant || "other"}_description`)}
className="mb-6"
actions={
<Button
href={
variant
? `/apps/categories/${variant === "conferencing" ? "video" : variant}`
: "/apps"
}
color="secondary"
StartIcon={FiPlus}>
{t("add")}
</Button>
<ShellSubHeading
title={t(variant || "other")}
subtitle={t(`installed_app_${variant || "other"}_description`)}
className="mb-6"
actions={
<Button
href={
variant ? `/apps/categories/${variant === "conferencing" ? "video" : variant}` : "/apps"
}
/>
<IntegrationsList handleDisconnect={handleDisconnect} data={data} variant={variant} />
</div>
) : (
<EmptyScreen
Icon={emptyIcon[variant || "other"]}
headline={t("no_category_apps", {
category: (variant && t(variant).toLowerCase()) || t("other").toLowerCase(),
})}
description={t(`no_category_apps_description_${variant || "other"}`)}
buttonRaw={
<Button
color="secondary"
data-testid={`connect-${variant || "other"}-apps`}
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
{t(`connect_${variant || "other"}_apps`)}
</Button>
}
/>
)}
color="secondary"
StartIcon={FiPlus}>
{t("add")}
</Button>
}
/>
<IntegrationsList handleDisconnect={handleDisconnect} data={data} variant={variant} />
</>
);
}}
@@ -257,17 +311,19 @@ export default function InstalledApps() {
return (
<>
<InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
{categoryList.includes(category) && (
<IntegrationsContainer handleDisconnect={handleDisconnect} variant={category} />
)}
{category === "calendar" && <CalendarListContainer />}
{category === "other" && (
<IntegrationsContainer
handleDisconnect={handleDisconnect}
variant={category}
exclude={[...categoryList, "calendar"]}
/>
)}
<div className="rounded-md border border-gray-200 p-7">
{categoryList.includes(category) && (
<IntegrationsContainer handleDisconnect={handleDisconnect} variant={category} />
)}
{category === "calendar" && <CalendarListContainer />}
{category === "other" && (
<IntegrationsContainer
handleDisconnect={handleDisconnect}
variant={category}
exclude={[...categoryList, "calendar"]}
/>
)}
</div>
</InstalledAppsLayout>
<DisconnectIntegrationModal
handleModelClose={handleModelClose}
+1 -2
View File
@@ -6,11 +6,10 @@ import Link from "next/link";
import React, { useMemo } from "react";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { Button, TextField } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale";
import AuthContainer from "@components/ui/AuthContainer";
type Props = {
@@ -5,10 +5,10 @@ import Link from "next/link";
import { useRouter } from "next/router";
import React, { SyntheticEvent } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, EmailField } from "@calcom/ui";
import { getSession } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale";
import AuthContainer from "@components/ui/AuthContainer";
+7 -6
View File
@@ -19,6 +19,7 @@ import { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
import { FiArrowLeft } from "@calcom/ui/components/icon";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import withNonce, { WithNonceProps } from "@lib/withNonce";
import AddToHomescreen from "@components/AddToHomescreen";
import TwoFactor from "@components/auth/TwoFactor";
@@ -40,7 +41,7 @@ export default function Login({
isSAMLLoginEnabled,
samlTenantID,
samlProductID,
}: inferSSRProps<typeof getServerSideProps>) {
}: inferSSRProps<typeof _getServerSideProps> & WithNonceProps) {
const { t } = useLocale();
const router = useRouter();
const methods = useForm<LoginValues>();
@@ -181,8 +182,6 @@ export default function Login({
StartIcon={FaGoogle}
onClick={async (e) => {
e.preventDefault();
// track Google logins. Without personal data/payload
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
await signIn("google");
}}>
{t("signin_with_google")}
@@ -205,7 +204,8 @@ export default function Login({
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
// TODO: Once we understand how to retrieve prop types automatically from getServerSideProps, remove this temporary variable
const _getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) {
const { req } = context;
const session = await getSession({ req });
const ssr = await ssrInit(context);
@@ -229,7 +229,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
},
};
}
return {
props: {
csrfToken: await getCsrfToken(context),
@@ -240,4 +239,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
samlProductID,
},
};
}
};
export const getServerSideProps = withNonce(_getServerSideProps);
+123 -13
View File
@@ -1,40 +1,127 @@
import { UserPermissionRole } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { WizardForm } from "@calcom/ui";
import { Meta, WizardForm } from "@calcom/ui";
import SetupFormStep1 from "./steps/SetupFormStep1";
import StepDone from "./steps/StepDone";
import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
import ChooseLicense from "@components/setup/ChooseLicense";
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
import { ssrInit } from "@server/lib/ssr";
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
const shouldDisable = props.userCount !== 0;
const router = useRouter();
const [value, setValue] = useState(props.isFreeLicense ? "FREE" : "EE");
const isFreeLicense = value === "FREE";
const [isEnabledEE, setIsEnabledEE] = useState(!props.isFreeLicense);
const setStep = (newStep: number) => {
router.replace(`/auth/setup?step=${newStep || 1}`, undefined, { shallow: true });
};
const steps = [
const steps: React.ComponentProps<typeof WizardForm>["steps"] = [
{
title: t("administrator_user"),
description: t("lets_create_first_administrator_user"),
content: shouldDisable ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
isLoading: isLoadingStep1,
content: (setIsLoading) => (
<AdminUser
onSubmit={() => {
setIsLoading(true);
}}
onSuccess={() => {
setStep(2);
}}
onError={() => {
setIsLoading(false);
}}
userCount={props.userCount}
/>
),
},
{
title: t("enable_apps"),
description: t("enable_apps_description"),
content: <AdminAppsList baseURL="/auth/setup?step=2" useQueryParam={true} />,
isLoading: false,
title: t("choose_a_license"),
description: t("choose_license_description"),
content: (setIsLoading) => {
return (
<ChooseLicense
id="wizard-step-2"
name="wizard-step-2"
value={value}
onChange={setValue}
onSubmit={() => {
setIsLoading(true);
setStep(3);
}}
/>
);
},
},
];
if (!isFreeLicense) {
steps.push({
title: t("step_enterprise_license"),
description: t("step_enterprise_license_description"),
content: (setIsLoading) => {
const currentStep = 3;
return (
<EnterpriseLicense
id={`wizard-step-${currentStep}`}
name={`wizard-step-${currentStep}`}
onSubmit={() => {
setIsLoading(true);
}}
onSuccess={() => {
setStep(currentStep + 1);
}}
onSuccessValidate={() => {
setIsEnabledEE(true);
}}
/>
);
},
isEnabled: isEnabledEE,
});
}
steps.push({
title: t("enable_apps"),
description: t("enable_apps_description"),
contentClassname: "!pb-0 mb-[-1px]",
content: (setIsLoading) => {
const currentStep = isFreeLicense ? 3 : 4;
return (
<AdminAppsList
id={`wizard-step-${currentStep}`}
name={`wizard-step-${currentStep}`}
classNames={{
form: "mb-4 rounded-md bg-white px-0 pt-0 md:max-w-full",
appCategoryNavigationContainer: "max-h-[400px] overflow-y-auto md:p-4",
verticalTabsItem: "!w-48 md:p-4",
}}
baseURL={`/auth/setup?step=${currentStep}`}
useQueryParam={true}
onSubmit={() => {
setIsLoading(true);
router.replace("/");
}}
/>
);
},
});
return (
<>
<main className="flex items-center bg-gray-100 print:h-full">
<Meta title={t("setup")} description={t("setup_description")} />
<main className="flex items-center bg-gray-100 print:h-full md:h-screen">
<WizardForm
href="/auth/setup"
steps={steps}
@@ -49,6 +136,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const userCount = await prisma.user.count();
const { req } = context;
const session = await getSession({ req });
@@ -62,8 +150,30 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
};
}
let deploymentKey = await getDeploymentKey(prisma);
// Check existant CALCOM_LICENSE_KEY env var and acccount for it
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey) {
await prisma.deployment.upsert({
where: { id: 1 },
update: {
licenseKey: process.env.CALCOM_LICENSE_KEY,
agreedLicenseAt: new Date(),
},
create: {
licenseKey: process.env.CALCOM_LICENSE_KEY,
agreedLicenseAt: new Date(),
},
});
deploymentKey = await getDeploymentKey(prisma);
}
const isFreeLicense = deploymentKey === "";
return {
props: {
trpcState: ssr.dehydrate(),
isFreeLicense,
userCount,
},
};
+4 -4
View File
@@ -148,7 +148,7 @@ export default function Availability({ schedule }: { schedule: number }) {
)
}
CTA={
<div className="mr-10 flex items-center justify-end xl:mr-36">
<div className="flex items-center justify-end">
<div className="flex items-center rounded-md px-2 sm:hover:bg-gray-100">
<Skeleton
as={Label}
@@ -186,7 +186,7 @@ export default function Availability({ schedule }: { schedule: number }) {
});
}}
className="flex flex-col sm:mx-0 xl:flex-row xl:space-x-6">
<div className="mx-10 flex-1 flex-row xl:mr-0 xl:ml-36">
<div className="flex-1 flex-row xl:mr-0">
<div className="mb-6 rounded-md border">
<div>
{typeof me.data?.weekStart === "string" && (
@@ -206,7 +206,7 @@ export default function Availability({ schedule }: { schedule: number }) {
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
</div>
</div>
<div className="min-w-40 col-span-3 ml-10 space-y-2 lg:col-span-1 xl:ml-36">
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
@@ -228,7 +228,7 @@ export default function Availability({ schedule }: { schedule: number }) {
/>
</div>
<hr className="my-6 mr-8" />
<div className="rounded-md">
<div className="hidden rounded-md md:block">
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
<div className="mt-3 flex">
<Button href="/availability/troubleshoot" color="secondary">
+73 -18
View File
@@ -11,7 +11,11 @@ import { RRule } from "rrule";
import { z } from "zod";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import { getEventLocationValue, getSuccessPageLocationMessage } from "@calcom/app-store/locations";
import {
getEventLocationValue,
getSuccessPageLocationMessage,
guessEventLocationType,
} from "@calcom/app-store/locations";
import { getEventTypeAppData } from "@calcom/app-store/utils";
import { getEventName } from "@calcom/core/event";
import dayjs, { ConfigType } from "@calcom/dayjs";
@@ -34,9 +38,10 @@ import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/t
import { localStorage } from "@calcom/lib/webstorage";
import prisma from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Button, EmailInput, HeadSeo } from "@calcom/ui";
import { FiX, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -188,19 +193,22 @@ export default function Success(props: SuccessProps) {
window.scrollTo(0, document.body.scrollHeight);
}
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(props.bookingInfo.location)
? props.bookingInfo.location[0]
const location: ReturnType<typeof getEventLocationValue> = props.bookingInfo.location
? props.bookingInfo.location
: // If there is no location set then we default to Cal Video
"integrations:daily";
const locationVideoCallUrl: string | undefined = bookingMetadataSchema.parse(
props?.bookingInfo?.metadata || {}
)?.videoCallUrl;
if (!location) {
// Can't use logger.error because it throws error on client. stdout isn't available to it.
console.error(`No location found `);
}
const status = props.bookingInfo?.status;
const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED;
const cancellationReason = props.bookingInfo.cancellationReason;
const cancellationReason = props.bookingInfo.cancellationReason || props.bookingInfo.rejectionReason;
const attendeeName =
typeof props?.bookingInfo?.attendees?.[0]?.name === "string"
@@ -240,7 +248,7 @@ export default function Success(props: SuccessProps) {
const giphyAppData = getEventTypeAppData(eventType, "giphy");
const giphyImage = giphyAppData?.thankYouPage;
const eventName = getEventName(eventNameObject);
const eventName = getEventName(eventNameObject, true);
const needsConfirmation = eventType.requiresConfirmation && reschedule != true;
const isCancelled = status === "CANCELLED" || status === "REJECTED";
const telemetry = useTelemetry();
@@ -281,8 +289,8 @@ export default function Success(props: SuccessProps) {
function eventLink(): string {
const optional: { location?: string } = {};
if (location) {
optional["location"] = location;
if (locationVideoCallUrl) {
optional["location"] = locationVideoCallUrl;
}
const event = createEvent({
@@ -332,12 +340,17 @@ export default function Success(props: SuccessProps) {
);
const customInputs = bookingInfo?.customInputs;
const locationToDisplay = getSuccessPageLocationMessage(location, t);
const locationToDisplay = getSuccessPageLocationMessage(
locationVideoCallUrl ? locationVideoCallUrl : location,
t,
bookingInfo.status
);
const hasSMSAttendeeAction =
eventType.workflows.find((workflowEventType) =>
workflowEventType.workflow.steps.find((step) => step.action === WorkflowActions.SMS_ATTENDEE)
) !== undefined;
const providerName = guessEventLocationType(location)?.label;
return (
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
@@ -427,6 +440,17 @@ export default function Success(props: SuccessProps) {
<div className="mt-3">
<p className="text-gray-600 dark:text-gray-300">{getTitle()}</p>
</div>
{props.paymentStatus && (
<h4>
{!props.paymentStatus.success &&
!props.paymentStatus.refunded &&
t("booking_with_payment_cancelled")}
{props.paymentStatus.success &&
!props.paymentStatus.refunded &&
t("booking_with_payment_cancelled_already_paid")}
{props.paymentStatus.refunded && t("booking_with_payment_cancelled_refunded")}
</h4>
)}
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-200 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
{(isCancelled || reschedule) && cancellationReason && (
@@ -490,8 +514,14 @@ export default function Success(props: SuccessProps) {
<div className="mt-3 font-medium">{t("where")}</div>
<div className="col-span-2 mt-3">
{locationToDisplay.startsWith("http") ? (
<a title="Meeting Link" href={locationToDisplay}>
{locationToDisplay}
<a
href={locationToDisplay}
target="_blank"
title={locationToDisplay}
className="flex items-center gap-2 text-gray-700 underline dark:text-gray-50"
rel="noreferrer">
{providerName || "Link"}
<FiExternalLink className="inline h-4 w-4 text-gray-700 dark:text-white" />
</a>
) : (
locationToDisplay
@@ -517,7 +547,7 @@ export default function Success(props: SuccessProps) {
<>
{eventTypeCustomFound?.type === "RADIO" && (
<>
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
<div className="border-bookinglightest dark:border-darkgray-300 col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
{eventTypeCustomFound.label}
</div>
<div className="col-span-3 mt-1 mb-2">
@@ -541,7 +571,9 @@ export default function Success(props: SuccessProps) {
)}
{eventTypeCustomFound?.type !== "RADIO" && customInput !== "" && (
<>
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">{key}</div>
<div className="border-bookinglightest dark:border-darkgray-300 col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
{key}
</div>
<div className="col-span-3 mt-2 mb-2">
{typeof customInput === "boolean" ? (
<p>{customInput ? "true" : "false"}</p>
@@ -632,8 +664,8 @@ export default function Success(props: SuccessProps) {
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
props.eventType.description
}` +
(typeof location === "string"
? "&location=" + encodeURIComponent(location)
(typeof locationVideoCallUrl === "string"
? "&location=" + encodeURIComponent(locationVideoCallUrl)
: "") +
(props.eventType.recurringEvent
? "&recur=" +
@@ -661,7 +693,10 @@ export default function Success(props: SuccessProps) {
date.utc().format() +
"&subject=" +
eventName
) + (location ? "&location=" + location : "")
) +
(locationVideoCallUrl
? "&location=" + encodeURIComponent(locationVideoCallUrl)
: "")
}
className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
target="_blank">
@@ -685,7 +720,10 @@ export default function Success(props: SuccessProps) {
date.utc().format() +
"&subject=" +
eventName
) + (location ? "&location=" + location : "")
) +
(locationVideoCallUrl
? "&location=" + encodeURIComponent(locationVideoCallUrl)
: "")
}
className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
target="_blank">
@@ -984,7 +1022,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
endTime: true,
location: true,
status: true,
metadata: true,
cancellationReason: true,
rejectionReason: true,
user: {
select: {
id: true,
@@ -1087,6 +1127,20 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
});
}
const payment = await prisma.payment.findFirst({
where: {
bookingId: bookingInfo.id,
},
select: {
success: true,
refunded: true,
},
});
const paymentStatus = {
success: payment?.success,
refunded: payment?.refunded,
};
return {
props: {
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
@@ -1096,6 +1150,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
trpcState: ssr.dehydrate(),
dynamicEventName: bookingInfo?.eventType?.eventName || "",
bookingInfo,
paymentStatus: payment ? paymentStatus : null,
},
};
}
+3 -2
View File
@@ -88,6 +88,8 @@ export default function Bookings() {
return false;
}
shownBookings[booking.recurringEventId] = [booking];
} else if (status === "upcoming") {
return new Date(booking.startTime).toDateString() !== new Date().toDateString();
}
return true;
};
@@ -121,7 +123,7 @@ export default function Bookings() {
<p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("today")}</p>
<div className="overflow-hidden rounded-md border border-gray-200">
<table className="w-full max-w-full table-fixed">
<tbody className="divide-y divide-gray-200 bg-white">
<tbody className="divide-y divide-gray-200 bg-white" data-testid="today-bookings">
<Fragment>
{bookingsToday.map((booking: BookingOutput) => (
<BookingListItem
@@ -138,7 +140,6 @@ export default function Bookings() {
</div>
)}
<div className="pt-2 xl:pt-0">
{/* <p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("all")}</p> */}
<div className="overflow-hidden rounded-md border border-gray-200">
<table className="w-full max-w-full table-fixed">
<tbody className="divide-y divide-gray-200 bg-white" data-testid="bookings">
+1 -1
View File
@@ -62,7 +62,7 @@ export type FormValues = {
phone?: string;
}[];
customInputs: CustomInputParsed[];
schedule: number;
schedule: number | null;
periodType: PeriodType;
periodDays: number;
periodCountCalendarDays: "1" | "0";
+31 -43
View File
@@ -136,7 +136,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
)}
</div>
<EventTypeDescription
// @ts-expect-error FIXME We have a type mismtach here @hariombalhara @sean-brydon
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
eventType={type}
/>
</Link>
@@ -386,15 +386,13 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
/>
</Tooltip>
<Dropdown modal={false}>
<DropdownMenuTrigger
asChild
data-testid={"event-type-options-" + type.id}
className="radix-state-open:rounded-r-md">
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
<Button
type="button"
variant="icon"
color="secondary"
StartIcon={FiMoreHorizontal}
className="ltr:radix-state-open:rounded-r-md rtl:radix-state-open:rounded-l-md"
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
@@ -457,37 +455,30 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownMenuPortal>
<DropdownMenuContent>
<DropdownMenuItem className="outline-none">
<Link href={calLink} target="_blank">
<Button
color="minimal"
StartIcon={FiExternalLink}
className="w-full rounded-none">
{t("preview")}
</Button>
</Link>
<DropdownItem
href={calLink}
target="_blank"
StartIcon={FiExternalLink}
className="w-full rounded-none">
{t("preview")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="outline-none">
<Button
type="button"
color="minimal"
className="w-full rounded-none text-left"
<DropdownItem
data-testid={"event-type-duplicate-" + type.id}
StartIcon={FiClipboard}
onClick={() => {
navigator.clipboard.writeText(calLink);
showToast(t("link_copied"), "success");
}}>
}}
StartIcon={FiClipboard}
className="w-full rounded-none text-left">
{t("copy_link")}
</Button>
</DropdownItem>
</DropdownMenuItem>
{isNativeShare ? (
<DropdownMenuItem className="outline-none">
<Button
type="button"
color="minimal"
className="w-full rounded-none"
<DropdownItem
data-testid={"event-type-duplicate-" + type.id}
StartIcon={FiUpload}
onClick={() => {
navigator
.share({
@@ -497,44 +488,41 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
})
.then(() => showToast(t("link_shared"), "success"))
.catch(() => showToast(t("failed"), "error"));
}}>
}}
StartIcon={FiUpload}
className="w-full rounded-none">
{t("share")}
</Button>
</DropdownItem>
</DropdownMenuItem>
) : null}
<DropdownMenuItem className="outline-none">
<Button
type="button"
<DropdownItem
onClick={() => router.push("/event-types/" + type.id)}
color="minimal"
className="w-full rounded-none"
StartIcon={FiEdit}>
StartIcon={FiEdit}
className="w-full rounded-none">
{t("edit")}
</Button>
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="outline-none">
<Button
type="button"
color="minimal"
className="w-full rounded-none"
data-testid={"event-type-duplicate-" + type.id}
<DropdownItem
onClick={() => openDuplicateModal(type, group)}
StartIcon={FiCopy}
onClick={() => openDuplicateModal(type, group)}>
data-testid={"event-type-duplicate-" + type.id}>
{t("duplicate")}
</Button>
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="outline-none">
<Button
<DropdownItem
color="destructive"
onClick={() => {
setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id);
}}
color="destructive"
StartIcon={FiTrash}
className="w-full rounded-none">
{t("delete")}
</Button>
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuPortal>
@@ -105,7 +105,7 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
</p>
))}
</header>
<Steps maxSteps={steps.length} currentStep={currentStepIndex} navigateToStep={goToIndex} />
<Steps maxSteps={steps.length} currentStep={currentStepIndex + 1} navigateToStep={goToIndex} />
</div>
<StepCard>
{currentStep === "user-settings" && <UserSettings user={user} nextStep={() => goToIndex(1)} />}
+27
View File
@@ -0,0 +1,27 @@
import Head from "next/head";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
export default function MaintenancePage() {
const { t, isLocaleReady } = useLocale();
if (!isLocaleReady) return null;
return (
<div className="flex h-screen bg-gray-100">
<Head>
<title>
{t("under_maintenance")} | {APP_NAME}
</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="m-auto rounded-md bg-white p-10 text-right ltr:text-left">
<h1 className="text-2xl font-medium text-black">{t("under_maintenance")}</h1>
<p className="mt-4 mb-6 max-w-2xl text-sm text-gray-600">
{t("under_maintenance_description", { appName: APP_NAME })}
</p>
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
</div>
</div>
);
}
@@ -1,6 +1,7 @@
import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import { EventLocationType, getEventLocationTypeFromApp } from "@calcom/app-store/locations";
import { AppSetDefaultLinkDailog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
@@ -21,12 +22,10 @@ import {
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { FiAlertCircle, FiMoreHorizontal, FiTrash } from "@calcom/ui/components/icon";
import { FiAlertCircle, FiMoreHorizontal, FiTrash, FiVideo } from "@calcom/ui/components/icon";
import AppListCard from "@components/AppListCard";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>
@@ -43,12 +42,13 @@ const ConferencingLayout = () => {
const { t } = useLocale();
const utils = trpc.useContext();
const { data: apps, isLoading } = trpc.viewer.integrations.useQuery(
{ variant: "conferencing", onlyInstalled: true },
{
suspense: true,
}
);
const { data: defaultConferencingApp, isLoading: defaultConferencingAppLoading } =
trpc.viewer.getUsersDefaultConferencingApp.useQuery();
const { data: apps, isLoading } = trpc.viewer.integrations.useQuery({
variant: "conferencing",
onlyInstalled: true,
});
const deleteAppMutation = trpc.viewer.deleteCredential.useMutation({
onSuccess: () => {
showToast("Integration deleted successfully", "success");
@@ -61,10 +61,23 @@ const ConferencingLayout = () => {
},
});
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
onSuccess: () => {
showToast("Default app updated successfully", "success");
utils.viewer.getUsersDefaultConferencingApp.invalidate();
},
onError: (error) => {
showToast(`Error: ${error.message}`, "error");
},
});
const [deleteAppModal, setDeleteAppModal] = useState(false);
const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
undefined
);
const [deleteCredentialId, setDeleteCredentialId] = useState<number>(0);
if (isLoading)
if (isLoading || defaultConferencingAppLoading)
return <SkeletonLoader title={t("conferencing")} description={t("conferencing_description")} />;
return (
@@ -74,39 +87,67 @@ const ConferencingLayout = () => {
{apps?.items &&
apps.items
.map((app) => ({ ...app, title: app.title || app.name }))
.map((app) => (
<AppListCard
description={app.description}
title={app.title}
logo={app.logo}
key={app.title}
isDefault={app.isGlobal}
actions={
<div>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownItem
type="button"
color="destructive"
disabled={app.isGlobal}
StartIcon={FiTrash}
onClick={() => {
setDeleteCredentialId(app.credentialIds[0]);
setDeleteAppModal(true);
}}>
{t("remove_app")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
}
/>
))}
.map((app) => {
const appSlug = app?.slug;
const appIsDefault =
appSlug === defaultConferencingApp?.appSlug ||
(appSlug === "daily-video" && !defaultConferencingApp?.appSlug); // Default to cal video if the user doesnt have it set (we do this on new account creation but not old)
return (
<AppListCard
description={app.description}
title={app.title}
logo={app.logo}
key={app.title}
isDefault={appIsDefault} // @TODO: Handle when a user doesnt have this value set
actions={
<div>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{!appIsDefault && (
<DropdownMenuItem>
<DropdownItem
type="button"
color="secondary"
StartIcon={FiVideo}
onClick={() => {
const locationType = getEventLocationTypeFromApp(
app?.locationOption?.value ?? ""
);
if (locationType?.linkType === "static") {
setLocationType({ ...locationType, slug: appSlug });
} else {
updateDefaultAppMutation.mutate({
appSlug,
});
}
}}>
{t("change_default_conferencing_app")}
</DropdownItem>
</DropdownMenuItem>
)}
<DropdownMenuItem>
<DropdownItem
type="button"
color="destructive"
disabled={app.isGlobal}
StartIcon={FiTrash}
onClick={() => {
setDeleteCredentialId(app.credentialIds[0]);
setDeleteAppModal(true);
}}>
{t("remove_app")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
}
/>
);
})}
</List>
<Dialog open={deleteAppModal} onOpenChange={setDeleteAppModal}>
@@ -123,20 +164,14 @@ const ConferencingLayout = () => {
</DialogFooter>
</DialogContent>
</Dialog>
{locationType && (
<AppSetDefaultLinkDailog locationType={locationType} setLocationType={setLocationType} />
)}
</div>
);
};
ConferencingLayout.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default ConferencingLayout;
@@ -69,7 +69,9 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
const { t } = useLocale();
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: () => {
onSuccess: async () => {
// Invalidate our previous i18n cache
await utils.viewer.public.i18n.invalidate();
reset(getValues());
showToast(t("settings_updated_successfully"), "success");
},
+45 -12
View File
@@ -1,7 +1,6 @@
import { IdentityProvider } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next";
import { useState } from "react";
import { signOut, useSession } from "next-auth/react";
import { useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
@@ -9,7 +8,7 @@ import { identityProviderNameMap } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { userMetadata } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import { Button, Form, Meta, PasswordField, Select, SettingsToggle, showToast } from "@calcom/ui";
import { Alert, Button, Form, Meta, PasswordField, Select, SettingsToggle, showToast } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
@@ -17,9 +16,11 @@ type ChangePasswordSessionFormValues = {
oldPassword: string;
newPassword: string;
sessionTimeout?: number;
apiError: string;
};
const PasswordView = () => {
const { data } = useSession();
const { t } = useLocale();
const utils = trpc.useContext();
const { data: user } = trpc.viewer.me.useQuery();
@@ -58,9 +59,24 @@ const PasswordView = () => {
showToast(t("password_has_been_changed"), "success");
formMethods.resetField("oldPassword");
formMethods.resetField("newPassword");
if (data?.user.role === "INACTIVE_ADMIN") {
/*
AdminPasswordBanner component relies on the role returned from the session.
Next-Auth doesn't provide a way to revalidate the session cookie,
so this a workaround to hide the banner after updating the password.
discussion: https://github.com/nextauthjs/next-auth/discussions/4229
*/
signOut({ callbackUrl: "/auth/login" });
}
},
onError: (error) => {
showToast(`${t("error_updating_password")}, ${error.message}`, "error");
showToast(`${t("error_updating_password")}, ${t(error.message)}`, "error");
formMethods.setError("apiError", {
message: t(error.message),
type: "custom",
});
},
});
@@ -91,6 +107,8 @@ const PasswordView = () => {
const isDisabled = formMethods.formState.isSubmitting || !formMethods.formState.isDirty;
const passwordMinLength = data?.user.role === "USER" ? 7 : 15;
const isUser = data?.user.role === "USER";
return (
<>
<Meta title={t("password")} description={t("password_description")} />
@@ -111,19 +129,34 @@ const PasswordView = () => {
</div>
) : (
<Form form={formMethods} handleSubmit={handleSubmit}>
<div className="max-w-[38rem] sm:flex sm:space-x-4">
<div className="flex-grow">
{formMethods.formState.errors.apiError && (
<div className="pb-6">
<Alert severity="error" message={formMethods.formState.errors.apiError?.message} />
</div>
)}
<div className="max-w-[38rem] sm:grid sm:grid-cols-2 sm:gap-x-4">
<div>
<PasswordField {...formMethods.register("oldPassword")} label={t("old_password")} />
</div>
<div className="flex-grow">
<PasswordField {...formMethods.register("newPassword")} label={t("new_password")} />
<div>
<PasswordField
{...formMethods.register("newPassword", {
minLength: {
message: t(isUser ? "password_hint_min" : "password_hint_admin_min"),
value: passwordMinLength,
},
pattern: {
message: "Should contain a number, uppercase and lowercase letters",
value: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).*$/gm,
},
})}
label={t("new_password")}
/>
</div>
</div>
<p className="mt-4 max-w-[38rem] text-sm text-gray-600">
<Trans i18nKey="invalid_password_hint">
Password must be at least at least 7 characters, mix of uppercase & lowercase letters, and
contain at least 1 number.
</Trans>
{t("invalid_password_hint", { passwordLength: passwordMinLength })}
</p>
<div className="mt-8 border-t border-gray-200 py-8">
<SettingsToggle
+1 -1
View File
@@ -56,7 +56,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
})
.then(handleErrors)
.then(async () => {
telemetry.event(telemetryEventTypes.login, collectPageParameters());
telemetry.event(telemetryEventTypes.signup, collectPageParameters());
await signIn<"credentials">("credentials", {
...data,
callbackUrl: router.query.callbackUrl
+19 -1
View File
@@ -100,6 +100,22 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
availability: true,
},
},
team: {
select: {
members: {
where: {
role: "OWNER",
},
select: {
user: {
select: {
weekStart: true,
},
},
},
},
},
},
},
},
},
@@ -148,6 +164,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
booking = await getBooking(prisma, rescheduleUid);
}
const weekStart = eventType.team?.members?.[0]?.user?.weekStart;
return {
props: {
profile: {
@@ -155,7 +173,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
slug: team.slug,
image: team.logo,
theme: team.theme,
weekStart: "Sunday",
weekStart: weekStart ?? "Sunday",
brandColor: team.brandColor,
darkBrandColor: team.darkBrandColor,
},
+5 -1
View File
@@ -22,7 +22,11 @@ export const createPaymentsFixture = (page: Page) => {
currency: "usd",
success,
refunded,
type: "STRIPE",
app: {
connect: {
slug: "stripe",
},
},
data: {},
externalId: "DEMO_PAYMENT_FROM_DB_" + Date.now(),
booking: {
+7 -4
View File
@@ -1,6 +1,6 @@
import type { Page, WorkerInfo } from "@playwright/test";
import type Prisma from "@prisma/client";
import { Prisma as PrismaType, MembershipRole } from "@prisma/client";
import { MembershipRole, Prisma as PrismaType } from "@prisma/client";
import { hash } from "bcryptjs";
import dayjs from "@calcom/dayjs";
@@ -34,11 +34,14 @@ const seededForm = {
type UserWithIncludes = PrismaType.UserGetPayload<typeof userWithEventTypes>;
const createTeamAndAddUser = async ({ user }: { user: { id: number; role?: MembershipRole } }) => {
const createTeamAndAddUser = async (
{ user }: { user: { id: number; role?: MembershipRole } },
workerInfo: WorkerInfo
) => {
const team = await prisma.team.create({
data: {
name: "",
slug: `team-${Date.now()}`,
slug: `team-${workerInfo.workerIndex}-${Date.now()}`,
},
});
if (!team) {
@@ -216,7 +219,7 @@ export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
include: userIncludes,
});
if (scenario.hasTeam) {
await createTeamAndAddUser({ user: { id: user.id, role: "OWNER" } });
await createTeamAndAddUser({ user: { id: user.id, role: "OWNER" } }, workerInfo);
}
const userFixture = createUserFixture(user, store.page!);
store.users.push(userFixture);
+1 -1
View File
@@ -24,7 +24,7 @@ test.describe("Onboarding", () => {
await page.locator("input[name=username]").fill("new user onboarding");
await page.locator("input[name=name]").fill("new user 2");
await page.locator("input[role=combobox]").click();
await page.locator("text=Eastern Time").click();
await page.locator("text=Europe/London GMT +0:00").click();
await page.locator("button[type=submit]").click();
+16 -1
View File
@@ -113,7 +113,22 @@ test.describe("Reschedule Tests", async () => {
status: BookingStatus.CANCELLED,
paid: false,
});
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
metadata: {
apps: {
stripe: {
price: 20000,
enabled: true,
currency: "usd",
},
},
},
},
});
const payment = await payments.create(booking.id);
await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${booking.uid}`);
+4 -11
View File
@@ -38,22 +38,15 @@ test.describe("Wipe my Cal App Test", () => {
await expect(page.locator("data-testid=wipe-today-button")).toBeVisible();
const $openBookingCount = await page.locator('[data-testid="bookings"] > *').count();
expect($openBookingCount).toBe(3);
const $todayBookingCount = await page.locator('[data-testid="today-bookings"] > *').count();
expect($openBookingCount + $todayBookingCount).toBe(3);
await page.locator("data-testid=wipe-today-button").click();
// Don't await send_request click, otherwise mutation can possibly occur before observer is attached
page.locator("data-testid=send_request").click();
const $openBookings = page.locator('[data-testid="bookings"]');
await $openBookings.evaluate((ul) => {
return new Promise<void>((resolve) =>
new window.MutationObserver(() => {
if (ul.childElementCount === 2) {
resolve();
}
}).observe(ul, { childList: true })
);
});
// There will not be any today-bookings
await expect(page.locator('[data-testid="today-bookings"]')).toBeHidden();
await users.deleteAll();
});
+26
View File
@@ -0,0 +1,26 @@
const calEmbedMode = typeof new URL(document.URL).searchParams.get("embed") === "string";
/* Iframe Name */
window.name.includes("cal-embed");
window.isEmbed = () => {
// Once an embed mode always an embed mode
return calEmbedMode;
};
window.resetEmbedStatus = () => {
try {
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
window.sessionStorage.removeItem("calEmbedMode");
} catch (e) {}
};
window.getEmbedTheme = () => {
const url = new URL(document.URL);
return url.searchParams.get("theme");
};
window.getEmbedNamespace = () => {
const url = new URL(document.URL);
const namespace = url.searchParams.get("embed");
return namespace;
};
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} هو بديل Calendly مفتوح المصدر الذي يتيح لك التحكم في بياناتك وسير عملك والمظهر على التطبيق أو موقع الويب.",
"have_any_questions": "هل لديك أسئلة؟ نحن هنا للمساعدة.",
"reset_password_subject": "{{appName}}: إرشادات إعادة تعيين كلمة المرور",
"event_declined_subject": "تم الرفض: {{eventType}} مع {{name}} في {{date}}",
"event_cancelled_subject": "تم الإلغاء: {{eventType}} مع {{name}} في {{date}}",
"event_declined_subject": "تم الرفض: {{title}} في {{date}}",
"event_cancelled_subject": "تم الإلغاء: {{title}} في {{date}}",
"event_request_declined": "تم رفض طلب الحدث الخاص بك",
"event_request_declined_recurring": "تم رفض طلب الحدث المتكرر الخاص بك",
"event_request_cancelled": "تم إلغاء حدثك المُجَدْوَل",
@@ -32,7 +32,7 @@
"refund_failed": "فشل استرداد مقابل الحدث {{eventType}} مع {{userName}} في {{date}}.",
"check_with_provider_and_user": "يُرجى التحقق من مزود الدفع لديك و{{user}} بشأن كيفية التعامل مع الأمر.",
"a_refund_failed": "فشل الاسترداد",
"awaiting_payment_subject": "في انتظار الدفع: {{eventType}} مع {{name}} في {{date}}",
"awaiting_payment_subject": "في انتظار الدفع: {{title}} في {{date}}",
"meeting_awaiting_payment": "اجتماعك في انتظار الدفع",
"help": "المساعدة",
"price": "السعر",
@@ -63,11 +63,11 @@
"someone_requested_an_event": "طلب شخص ما جدولة حدث في تقويمك.",
"someone_requested_password_reset": "طلب شخص ما رابطا لتغيير كلمة المرور الخاصة بك.",
"password_reset_instructions": "إذا لم تطلب هذا، يمكنك تجاهل هذا البريد الإلكتروني بأمان ولن يتم تغيير كلمة المرور الخاصة بك.",
"event_awaiting_approval_subject": "في انتظار الموافقة: {{eventType}} مع {{name}} في {{date}}",
"event_awaiting_approval_subject": "في انتظار الموافقة: {{title}} في {{date}}",
"event_still_awaiting_approval": "الحدث لا يزال في انتظار موافقتك",
"booking_submitted_subject": "تم إرسال الحجز: {{eventType}} مع {{name}} في {{date}}",
"booking_submitted_subject": "تم إرسال الحجز: {{title}} في {{date}}",
"your_meeting_has_been_booked": "لقد تم حجز الاجتماع الخاص بك",
"event_type_has_been_rescheduled_on_time_date": "تم إعادة جدولة {{eventType}} مع {{name}} إلى {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "تم إعادة جدولة {{title}} إلى {{date}}.",
"event_has_been_rescheduled": "تم التحديث - تم إعادة جدولة الحدث الخاص بك",
"request_reschedule_title_attendee": "طلب إعادة جدولة الحجز الخاص بك",
"request_reschedule_subtitle": "ألغى {{organizer}} الحجز وطلب منك اختيار حجز في وقت آخر.",
@@ -420,10 +420,10 @@
"forgotten_secret_description": "إذا فقدت هذا السر أو نسيته، يمكنك تغييره، ولكن ضع في اعتبارك أن جميع التكاملات التي تستخدم هذا السر ستحتاج إلى تحديث",
"current_incorrect_password": "كلمة المرور الحالية غير صحيحة",
"password_hint_caplow": "مزيج من الحروف الكبيرة والحروف الصغيرة",
"password_hint_min": "8 أحرف كحد أدنى",
"password_hint_min": "7 أحرف كحد أدنى",
"password_hint_admin_min": "15 محرفًا كحد أدنى",
"password_hint_num": "يحتوي على رقم واحد على الأقل",
"invalid_password_hint": "يجب أن تتألف كلمة المرور على الأقل من 7 أحرف، ورقم واحد على الأقل، ومزيج من الحروف الكبيرة والصغيرة",
"invalid_password_hint": "يجب أن تتألف كلمة المرور على الأقل من {{passwordLength}} أحرف، ورقم واحد على الأقل، ومزيج من الحروف الكبيرة والصغيرة",
"incorrect_password": "كلمة المرور غير صحيحة.",
"24_h": "24 ساعة",
"use_setting": "استخدام الإعداد",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} je open source alternativa, která vám dává kontrolu nad vlastními daty, workflow a vzhledem.",
"have_any_questions": "Máte otázky? Jsme tu, abychom vám pomohli.",
"reset_password_subject": "{{appName}}: Pokyny pro obnovení hesla",
"event_declined_subject": "Odmítnuto: {{eventType}} s {{name}} v {{date}}",
"event_cancelled_subject": "Zrušeno: {{eventType}} s {{name}} v {{date}}",
"event_declined_subject": "Odmítnuto: {{title}} v {{date}}",
"event_cancelled_subject": "Zrušeno: {{title}} v {{date}}",
"event_request_declined": "Žádost o událost byla zamítnuta",
"event_request_declined_recurring": "Žádost o opakovanou událost byla zamítnuta",
"event_request_cancelled": "Vaše naplánovaná událost byla zrušena",
@@ -32,7 +32,7 @@
"refund_failed": "Vrácení peněz za událost {{eventType}} pro {{userName}} během {{date}} selhalo.",
"check_with_provider_and_user": "Pro řešení prosím zkontaktujte vašeho správce plateb a/nebo uživatele {{user}}.",
"a_refund_failed": "Vrácení platby selhalo",
"awaiting_payment_subject": "Čekám na platbu: {{eventType}} s {{name}} během {{date}}",
"awaiting_payment_subject": "Čekám na platbu: {{title}} během {{date}}",
"meeting_awaiting_payment": "Vaše schůzka čeká na platbu",
"help": "Nápověda",
"price": "Cena",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Někdo požádal o naplánování události ve vašem kalendáři.",
"someone_requested_password_reset": "Někdo požádal o odkaz pro změnu vašeho hesla.",
"password_reset_instructions": "Pokud jste o obnovení hesla nepožádali, můžete tento e-mail ignorovat. Vaše heslo se nezmění.",
"event_awaiting_approval_subject": "Čeká se na schválení: {{eventType}} s {{name}} dne {{date}}",
"event_awaiting_approval_subject": "Čeká se na schválení: {{title}} dne {{date}}",
"event_still_awaiting_approval": "Událost stále čeká na schválení",
"booking_submitted_subject": "Rezervace odeslána: {{eventType}} s {{name}} během {{date}}",
"booking_submitted_subject": "Rezervace odeslána: {{title}} během {{date}}",
"your_meeting_has_been_booked": "Vaše schůzka byla rezervována",
"event_type_has_been_rescheduled_on_time_date": "Váš {{title}} byl přeplánován na {{date}}.",
"event_has_been_rescheduled": "Změna - Vaše událost byla přesunuta na jindy",
@@ -423,7 +423,7 @@
"password_hint_min": "Minimální délka 7 znaků",
"password_hint_admin_min": "Minimální délka 15 znaků",
"password_hint_num": "Obsahuje alespoň 1 číslo",
"invalid_password_hint": "Heslo musí být minimálně 7 znaků dlouhé, obsahovat alespoň jedno číslo a obsahovat kombinaci velkých a malých písmen",
"invalid_password_hint": "Heslo musí být minimálně {{passwordLength}} znaků dlouhé, obsahovat alespoň jedno číslo a obsahovat kombinaci velkých a malých písmen",
"incorrect_password": "Heslo není správné.",
"24_h": "24h",
"use_setting": "Použít nastavení",
@@ -8,7 +8,7 @@
"accept_invitation": "Einladung annehmen",
"have_any_questions": "Haben Sie Fragen? Wir sind hier um Ihnen zu helfen.",
"reset_password_subject": "{{appName}}: Anleitung zum Zurücksetzen des Passworts",
"event_declined_subject": "Abgelehnt: {{eventType}} mit {{name}} am {{date}}",
"event_declined_subject": "Abgelehnt: {{title}} am {{date}}",
"event_cancelled_subject": "Storniert: {{title}} um {{date}}",
"event_request_declined": "Ihre Event-Anfrage wurde abgelehnt",
"event_request_declined_recurring": "Ihre wiederkehrende Terminanfrage wurde abgelehnt",
@@ -31,7 +31,7 @@
"refund_failed": "Die Rückerstattung für den Termin {{eventType}} mit {{userName}} am {{date}} ist fehlgeschlagen.",
"check_with_provider_and_user": "Bitte überprüfen Sie bei Ihrem Zahlungsanbieter und {{userName}}, wie Sie damit umgehen.",
"a_refund_failed": "Erstattung fehlgeschlagen",
"awaiting_payment_subject": "Warte auf Zahlung: {{eventType}} mit {{name}} am {{date}}",
"awaiting_payment_subject": "Warte auf Zahlung: {{title}} am {{date}}",
"meeting_awaiting_payment": "Zahlung für den Termin noch offen",
"help": "Hilfe",
"price": "Preis",
@@ -62,9 +62,9 @@
"someone_requested_an_event": "Jemand hat eine Buchungsanfrage gestellt.",
"someone_requested_password_reset": "Jemand hat einen Link angefordert, um Ihr Passwort zu ändern.",
"password_reset_instructions": "Wenn Sie dies nicht angefordert haben, können Sie diese E-Mail sicher ignorieren und Ihr Passwort wird nicht geändert.",
"event_awaiting_approval_subject": "Warte auf Bestätigung: {{eventType}} mit {{name}} um {{date}}",
"event_awaiting_approval_subject": "Warte auf Bestätigung: {{title}} um {{date}}",
"event_still_awaiting_approval": "Ein Termin wartet noch auf Ihre Bestätigung",
"booking_submitted_subject": "Buchung eingereicht: {{eventType}} mit {{name}} am {{date}}",
"booking_submitted_subject": "Buchung eingereicht: {{title}} am {{date}}",
"your_meeting_has_been_booked": "Dein Termin wurde gebucht",
"event_type_has_been_rescheduled_on_time_date": "Ihr {{title}} wurde auf den {{date}} verschoben.",
"event_has_been_rescheduled": "Ihr Termin wurde verschoben.",
@@ -419,10 +419,10 @@
"forgotten_secret_description": "Wenn Sie dieses Passwort verloren oder vergessen haben, können Sie es ändern. Bitte beachten Sie, dass alle Integrationen, die dieses Geheimnis nutzen, aktualisiert werden müssen",
"current_incorrect_password": "Aktuelles Passwort ist falsch",
"password_hint_caplow": "Mischung aus Groß- und Kleinbuchstaben",
"password_hint_min": "Mindestens 8 Zeichen lang",
"password_hint_min": "Mindestens 7 Zeichen lang",
"password_hint_admin_min": "Mindestens 15 Zeichen lang",
"password_hint_num": "Enthält mindestens 1 Zahl",
"invalid_password_hint": "Das Passwort muss mindestens 7 Zeichen lang sein und mindestens eine Nummer beinhalten, sowie eine Mischung aus Groß- und Kleinbuchstaben sein",
"invalid_password_hint": "Das Passwort muss mindestens {{passwordLength}} Zeichen lang sein und mindestens eine Nummer beinhalten, sowie eine Mischung aus Groß- und Kleinbuchstaben sein",
"incorrect_password": "Passwort ist falsch",
"24_h": "24 Std",
"use_setting": "Benutze Einstellung",
@@ -7,8 +7,8 @@
"accept_invitation": "Αποδοχή Πρόσκλησης",
"have_any_questions": "Έχετε ερωτήσεις? Είμαστε εδώ για να βοηθήσουμε.",
"reset_password_subject": "{{appName}}: Οδηγίες επαναφοράς κωδικού πρόσβασης",
"event_declined_subject": "Απορρίφθηκε: {{eventType}} με {{name}} στις {{date}}",
"event_cancelled_subject": "Ακύρωση: {{eventType}} με {{name}} στις {{date}}",
"event_declined_subject": "Απορρίφθηκε: {{title}} στις {{date}}",
"event_cancelled_subject": "Ακύρωση: {{title}} στις {{date}}",
"need_to_reschedule_or_cancel": "Χρειάζεται να επαναπρογραμματίσετε ή να ακυρώσετε;",
"cancellation_reason": "Λόγος ακύρωσης (προαιρετικό)",
"rejection_reason": "Λόγος απόρριψης",
@@ -18,7 +18,7 @@
"refund_failed_subject": "Η επιστροφή χρημάτων απέτυχε: {{name}} - {{date}} - {{eventType}}",
"refund_failed": "Η επιστροφή χρημάτων για την εκδήλωση {{eventType}} με {{userName}} στις {{date}} απέτυχε.",
"a_refund_failed": "Αποτυχία επιστροφής χρημάτων",
"awaiting_payment_subject": "Σε αναμονή Πληρωμής: {{eventType}} με {{name}} στις {{date}}",
"awaiting_payment_subject": "Σε αναμονή Πληρωμής: {{title}} στις {{date}}",
"meeting_awaiting_payment": "Η πληρωμή της συνάντησής σας εκκρεμεί",
"help": "Βοήθεια",
"price": "Τιμή",
+44 -10
View File
@@ -428,10 +428,10 @@
"forgotten_secret_description": "If you have lost or forgotten this secret, you can change it, but be aware that all integrations using this secret will need to be updated",
"current_incorrect_password": "Current password is incorrect",
"password_hint_caplow": "Mix of uppercase & lowercase letters",
"password_hint_min": "Minimum 8 characters long",
"password_hint_min": "Minimum 7 characters long",
"password_hint_admin_min": "Minimum 15 characters long",
"password_hint_num": "Contain at least 1 number",
"invalid_password_hint": "The password must be a minimum of 7 characters long containing at least one number and have a mixture of uppercase and lowercase letters",
"invalid_password_hint": "The password must be a minimum of {{passwordLength}} characters long containing at least one number and have a mixture of uppercase and lowercase letters",
"incorrect_password": "Password is incorrect.",
"incorrect_username_password": "Username or password is incorrect.",
"24_h": "24h",
@@ -515,6 +515,8 @@
"admin": "Admin",
"administrator_user": "Administrator user",
"lets_create_first_administrator_user": "Let's create the first administrator user.",
"admin_user_created": "Administrator user setup",
"admin_user_created_description": "You have already created an administrator user. You can now log in to your account.",
"new_member": "New Member",
"invite": "Invite",
"add_team_members": "Add team members",
@@ -796,7 +798,7 @@
"number_apps_one": "{{count}} App",
"number_apps_other": "{{count}} Apps",
"trending_apps": "Trending Apps",
"most_popular":"Most Popular",
"most_popular": "Most Popular",
"explore_apps": "{{category}} apps",
"installed_apps": "Installed Apps",
"free_to_use_apps": "Free",
@@ -841,7 +843,7 @@
"connect_metamask": "Connect Metamask",
"create_events_on": "Create events on",
"enterprise_license": "This is an enterprise feature",
"enterprise_license_description": "To enable this feature, get a deployment key at {{consoleUrl}} console and add it to your .env as CALCOM_LICENSE_KEY. If your team already has a license, please contact {{supportMail}} for help.",
"enterprise_license_description": "To enable this feature, have an administrator go to {{setupUrl}} to enter a license key. If a license key is already in place, please contact {{supportMail}} for help.",
"missing_license": "Missing License",
"signup_requires": "Commercial license required",
"signup_requires_description": "{{companyName}} currently does not offer a free open source version of the sign up page. To receive full access to the signup components you need to acquire a commercial license. For personal use we recommend the Prisma Data Platform or any other Postgres interface to create accounts.",
@@ -1114,8 +1116,8 @@
"close": "Close",
"upgrade": "Upgrade",
"upgrade_to_access_recordings_title": "Upgrade to access recordings",
"upgrade_to_access_recordings_description":"Recordings are only available as part of our teams plan. Upgrade to start recording your calls",
"recordings_are_part_of_the_teams_plan":"Recordings are part of the teams plan",
"upgrade_to_access_recordings_description": "Recordings are only available as part of our teams plan. Upgrade to start recording your calls",
"recordings_are_part_of_the_teams_plan": "Recordings are part of the teams plan",
"team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.",
"team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.",
"show_eventtype_on_profile": "Show on Profile",
@@ -1414,7 +1416,7 @@
"team_url_required": "Must enter a team URL",
"team_url_taken": "This URL is already taken",
"team_publish": "Publish team",
"number_sms_notifications": "Phone number (SMS\u00a0notifications)",
"number_sms_notifications": "Phone number (SMS notifications)",
"attendee_email_variable": "Attendee email",
"attendee_email_info": "The person booking's email",
"kbar_search_placeholder": "Type a command or search...",
@@ -1451,6 +1453,9 @@
"disabled_calendar": "If you have another calendar installed new bookings will be added to it. If not then connect a new calendar so you do not miss any new bookings.",
"enable_apps": "Enable Apps",
"enable_apps_description": "Enable apps that users can integrate with Cal.com",
"purchase_license": "Purchase a License",
"already_have_key": "I already have a key:",
"already_have_key_suggestion": "Please copy your existing CALCOM_LICENSE_KEY environment variable here.",
"app_is_enabled": "{{appName}} is enabled",
"app_is_disabled": "{{appName}} is disabled",
"keys_have_been_saved": "Keys have been saved",
@@ -1496,11 +1501,11 @@
"not_verified": "Not yet verified",
"no_availability_in_month": "No availability in {{month}}",
"view_next_month": "View next month",
"send_code" : "Send code",
"send_code": "Send code",
"number_verified": "Number Verified",
"create_your_first_team_webhook_description": "Create your first webhook for this team event type",
"create_webhook_team_event_type": "Create a webhook for this team event type",
"disable_success_page":"Disable Success Page (only works if you have a redirect URL)",
"disable_success_page": "Disable Success Page (only works if you have a redirect URL)",
"invalid_admin_password": "You are set as an admin but you do not have a password length of at least 15 characters",
"change_password_admin": "Change Password to gain admin access",
"username_already_taken": "Username is already taken",
@@ -1526,6 +1531,25 @@
"reporting_feature": "See all incoming from data and download it as a CSV",
"teams_plan_required": "Teams plan required",
"routing_forms_are_a_great_way": "Routing forms are a great way to route your incoming leads to the right person. Upgrade to a Teams plan to access this feature.",
"choose_a_license": "Choose a license",
"choose_license_description": "Cal.com comes with an accessible and free AGPLv3 license which some limitations which can be upgraded to an Enterprise license at any time. You can upgrade at anytime later.",
"license": "License",
"agplv3_license": "AGPLv3 License",
"ee_enterprise_license": "“/ee” Enterprise License",
"enterprise_booking_fee": "Starting at {{enterprise_booking_fee}}/month",
"enterprise_license_includes": "Everything for a commercial use case",
"no_need_to_keep_your_code_open_source": "No need to keep your code open-source",
"repackage_rebrand_resell": "Repackage, rebrand and resell easily",
"a_vast_suite_of_enterprise_features": "A vast suite of enterprise features",
"free_license_fee": "$0.00/month",
"forever_open_and_free": "Forever Open & Free",
"required_to_keep_your_code_open_source": "Required to keep your code open source",
"cannot_repackage_and_resell": "Cannot repackage, rebrand and resell easily",
"no_enterprise_features": "No enterprise features",
"step_enterprise_license": "Enterprise License",
"step_enterprise_license_description": "Everything for a commercial use case with private hosting, repackaging, rebranding and reselling and access exclusive enterprise components.",
"setup": "Setup",
"setup_description": "Setup Cal.com instance",
"configure": "Configure",
"sso_configuration": "Single Sign-On",
"sso_configuration_description": "Configure SAML/OIDC SSO and allow team members to login using an Identity Provider",
@@ -1564,5 +1588,15 @@
"meetings_are_better_with_the_right": "Meetings are better with the right team members there. Invite them now.",
"create_a_one_one_template": "Create a one-one one template for an event type and distribute it to multiple members.",
"collective_or_roundrobin": "Collective or round-robin",
"book_your_team_members": "Book your team members together with collective events or cycle through to get the right person with round-robin."
"book_your_team_members": "Book your team members together with collective events or cycle through to get the right person with round-robin.",
"default_app_link_title":"Set a default app link",
"default_app_link_description":"Setting a default app link allows all newly created event types to use the app link you set.",
"change_default_conferencing_app":"Set as default",
"under_maintenance": "Down for maintenance",
"under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.",
"event_type_seats": "{{numberOfSeats}} seats",
"booking_with_payment_cancelled": "Paying for this event is no longer possible",
"booking_with_payment_cancelled_already_paid": "A refund for this booking payment it's on the way.",
"booking_with_payment_cancelled_refunded": "This booking payment has been refunded.",
"booking_confirmation_failed": "Booking confirmation failed"
}
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} es la alternativa de código abierto de Calendly poniéndole en control de sus propios datos, flujo de trabajo y apariencia.",
"have_any_questions": "¿Tienes preguntas? Estamos aquí para ayudar.",
"reset_password_subject": "{{appName}}: Instrucciones para restablecer la contraseña",
"event_declined_subject": "Rechazado: {{eventType}} con {{name}} en {{date}}",
"event_cancelled_subject": "Cancelado: {{eventType}} con {{name}} en {{date}}",
"event_declined_subject": "Rechazado: {{title}} en {{date}}",
"event_cancelled_subject": "Cancelado: {{title}} en {{date}}",
"event_request_declined": "Su solicitud de reunión ha sido rechazada",
"event_request_declined_recurring": "Su solicitud de evento recurrente se ha rechazado",
"event_request_cancelled": "Tu evento programado fue cancelado",
@@ -32,7 +32,7 @@
"refund_failed": "El reembolso del evento {{eventType}} con {{userName}} en {{date}} falló.",
"check_with_provider_and_user": "Por favor, compruebe con su proveedor de pago y {{userName}} cómo manejar esto.",
"a_refund_failed": "Un reembolso falló",
"awaiting_payment_subject": "Esperando pago: {{eventType}} con {{name}} en {{date}}",
"awaiting_payment_subject": "Esperando pago: {{title}} en {{date}}",
"meeting_awaiting_payment": "Su reunión está a la espera de pago",
"help": "Ayuda",
"price": "Precio",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Alguien ha solicitado programar un evento en su calendario.",
"someone_requested_password_reset": "Alguien ha solicitado un enlace para cambiar su contraseña.",
"password_reset_instructions": "Si usted no solicitó esto, puede ignorar este correo electrónico de forma segura y su contraseña no se cambiará.",
"event_awaiting_approval_subject": "Esperando aprobación: {{eventType}} con {{name}} en {{date}}",
"event_awaiting_approval_subject": "Esperando aprobación: {{title}} en {{date}}",
"event_still_awaiting_approval": "Un evento aún está esperando su aprobación",
"booking_submitted_subject": "Reserva enviada: {{eventType}} con {{name}} el {{date}}",
"booking_submitted_subject": "Reserva enviada:{{title}} el {{date}}",
"your_meeting_has_been_booked": "Su reunión ha sido reservada",
"event_type_has_been_rescheduled_on_time_date": "Tu {{title}} ha sido reprogramado para {{date}}.",
"event_has_been_rescheduled": "Tu evento ha sido reprogramado.",
@@ -423,7 +423,7 @@
"password_hint_min": "Mínimo 7 caracteres",
"password_hint_admin_min": "Longitud mínima de 15 caracteres",
"password_hint_num": "Debe contener al menos 1 número",
"invalid_password_hint": "La contraseña debe tener un mínimo de 7 caracteres que contengan al menos un número y una mezcla de letras mayúsculas y minúsculas",
"invalid_password_hint": "La contraseña debe tener un mínimo de {{passwordLength}} caracteres que contengan al menos un número y una mezcla de letras mayúsculas y minúsculas",
"incorrect_password": "La Contraseña es Incorrecta.",
"24_h": "24hs",
"use_setting": "Usar Ajuste",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Quelqu'un a demandé à planifier un événement sur votre calendrier.",
"someone_requested_password_reset": "Quelqu'un a demandé un lien pour modifier votre mot de passe.",
"password_reset_instructions": "Si vous n'en avez pas fait la demande, vous pouvez ignorer cet e-mail en toute sécurité et votre mot de passe ne sera pas modifié.",
"event_awaiting_approval_subject": "En attente de validation : {{eventType}} à {{date}}",
"event_awaiting_approval_subject": "En attente de validation : {{title}} à {{date}}",
"event_still_awaiting_approval": "Un événement est toujours en attente de votre validation",
"booking_submitted_subject": "Réservation confirmée : {{eventType}} avec {{name}} le {{date}}",
"booking_submitted_subject": "Réservation confirmée : {{title}} le {{date}}",
"your_meeting_has_been_booked": "Votre rendez-vous a été réservé",
"event_type_has_been_rescheduled_on_time_date": "Votre {{title}} a été replanifié à {{date}}.",
"event_has_been_rescheduled": "Mise à jour - Votre événement a été replanifié",
@@ -423,10 +423,10 @@
"forgotten_secret_description": "Vous pouvez modifier ce secret si vous l'avez perdu ou oublié. Soyez cependant conscient que toutes les intégrations l'utilisant devront être mises à jour.",
"current_incorrect_password": "Le mot de passe actuel est incorrect",
"password_hint_caplow": "Mélange de lettres majuscules et minuscules",
"password_hint_min": "Minimum 8 caractères",
"password_hint_min": "Minimum 7 caractères",
"password_hint_admin_min": "Minimum 15 caractères",
"password_hint_num": "Contient au moins 1 chiffre",
"invalid_password_hint": "Le mot de passe doit contenir au moins 7 caractères, un chiffre et un mélange de lettres, majuscules et minuscules.",
"invalid_password_hint": "Le mot de passe doit contenir au moins {{passwordLength}} caractères, un chiffre et un mélange de lettres, majuscules et minuscules.",
"incorrect_password": "Le mot de passe est incorrect.",
"24_h": "24 h",
"use_setting": "Utiliser le paramètre",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} הוא חלופת הקוד הפתוח של Calendly המאפשרת לך לשלוט בנתונים, בתהליך העבודה ובמראה של האתר שלך.",
"have_any_questions": "יש לך שאלות? אנחנו כאן כדי לעזור.",
"reset_password_subject": "{{appName}}: הנחיות לאיפוס סיסמה",
"event_declined_subject": "נדחה: {{eventType}} עם {{name}} בתאריך {{date}}",
"event_cancelled_subject": "בוטל: {{eventType}} עם {{name}} בתאריך {{date}}",
"event_declined_subject": "נדחה: {{title}} בתאריך {{date}}",
"event_cancelled_subject": "בוטל: {{title}} בתאריך {{date}}",
"event_request_declined": "בקשת האירוע שלך נדחתה",
"event_request_declined_recurring": "בקשת האירוע החוזר שלך נדחתה",
"event_request_cancelled": "האירוע המתוכנן בוטל",
@@ -32,7 +32,7 @@
"refund_failed": "ההחזר הכספי עבור האירוע {{eventType}} עם {{userName}} בתאריך {{date}} נכשל.",
"check_with_provider_and_user": "יש לברר עם ספק התשלום ועם {{user}} כיצד לטפל בעניין.",
"a_refund_failed": "ההחזר הכספי נכשל",
"awaiting_payment_subject": "בהמתנה לתשלום: {{eventType}} עם {{name}} בתאריך {{date}}",
"awaiting_payment_subject": "בהמתנה לתשלום: {{title}} בתאריך {{date}}",
"meeting_awaiting_payment": "התשלום על הפגישה שלך טרם בוצע",
"help": "עזרה",
"price": "מחיר",
@@ -63,11 +63,11 @@
"someone_requested_an_event": "מישהו ביקש לקבוע מועד לאירוע בלוח השנה שלך.",
"someone_requested_password_reset": "מישהו ביקש קישור כדי לשנות את הסיסמה שלך.",
"password_reset_instructions": "אם הבקשה לא היתה שלך, ניתן להתעלם מהודעת הדוא\"ל הזו בבטחה, והסיסמה שלך לא תשתנה.",
"event_awaiting_approval_subject": "בהמתנה לאישור: {{eventType}} עם {{name}} בתאריך {{date}}",
"event_awaiting_approval_subject": "בהמתנה לאישור: {{title}} בתאריך {{date}}",
"event_still_awaiting_approval": "אירוע עדיין ממתין לאישורך",
"booking_submitted_subject": "ההזמנה נשלחה: {{eventType}} עם {{name}} בתאריך {{date}}",
"booking_submitted_subject": "ההזמנה נשלחה: {{title}} בתאריך {{date}}",
"your_meeting_has_been_booked": "הפגישה שלך הוזמנה",
"event_type_has_been_rescheduled_on_time_date": "ה{{eventType}} שלך עם {{name}} הועבר לתאריך {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "{{title}} הועבר לתאריך {{date}}.",
"event_has_been_rescheduled": "עודכן – מועד האירוע שלך השתנה",
"request_reschedule_title_attendee": "בקשה לשינוי מועד ההזמנה",
"request_reschedule_subtitle": "{{organizer}} ביטל/ה את ההזמנה וביקש/ה שתבחר/י מועד אחר.",
@@ -420,10 +420,10 @@
"forgotten_secret_description": "אם שכחת או איבדת את הסוד, יש לך אפשרות לשנות אותו, אבל חשוב לזכור שיהיה צורך לעדכן את כל השילובים שבהם נעשה שימוש בסוד זה",
"current_incorrect_password": "הסיסמה הנוכחית שגויה",
"password_hint_caplow": "שילוב של אותיות רישיות וקטנות",
"password_hint_min": "באורך 8 תווים לפחות",
"password_hint_min": "באורך 7 תווים לפחות",
"password_hint_admin_min": "באורך של 15 תווים לפחות",
"password_hint_num": "לכלול לפחות ספרה אחת",
"invalid_password_hint": "הסיסמה חייבת להיות באורך של 7 תווים לפחות ולכלול לפחות ספרה אחת ושילוב של אותיות רישיות וקטנות",
"invalid_password_hint": "הסיסמה חייבת להיות באורך של {{passwordLength}} תווים לפחות ולכלול לפחות ספרה אחת ושילוב של אותיות רישיות וקטנות",
"incorrect_password": "הסיסמה שגויה.",
"24_h": "24 שעות",
"use_setting": "השתמש בהגדרה",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} è l'alternativa Calendly open source che ti consente di controllare i tuoi dati, il flusso di lavoro e l'aspetto.",
"have_any_questions": "Hai domande? Siamo qui per aiutare.",
"reset_password_subject": "{{appName}}: istruzioni per reimpostare la password",
"event_declined_subject": "Rifiutato: {{eventType}} con {{name}} il {{date}}",
"event_cancelled_subject": "Cancellato: {{eventType}} con {{name}} il {{date}}",
"event_declined_subject": "Rifiutato: {{title}} il {{date}}",
"event_cancelled_subject": "Cancellato: {{title}} il {{date}}",
"event_request_declined": "La tua richiesta per l'evento è stata rifiutata",
"event_request_declined_recurring": "La tua richiesta per l'evento ricorrente è stata rifiutata",
"event_request_cancelled": "Il tuo evento programmato è stato cancellato",
@@ -32,7 +32,7 @@
"refund_failed": "Il rimborso per l'evento {{eventType}} di {{userName}} del {{date}} non è riuscito.",
"check_with_provider_and_user": "Verifica con il tuo fornitore di servizi di pagamento e {{user}} come gestirlo.",
"a_refund_failed": "Un rimborso non è riuscito",
"awaiting_payment_subject": "In attesa del pagamento: {{eventType}} con {{name}} il {{date}}",
"awaiting_payment_subject": "In attesa del pagamento: {{title}} il {{date}}",
"meeting_awaiting_payment": "La tua riunione è in attesa di pagamento",
"help": "Guida",
"price": "Prezzo",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Qualcuno ha richiesto di programmare un evento sul tuo calendario.",
"someone_requested_password_reset": "Qualcuno ha richiesto un link per cambiare la tua password.",
"password_reset_instructions": "Se non l'hai richiesto, puoi tranquillamente ignorare questa email e la tua password non verrà modificata.",
"event_awaiting_approval_subject": "In attesa di approvazione: {{eventType}} con {{name}} il {{date}}",
"event_awaiting_approval_subject": "In attesa di approvazione: {{title}} il {{date}}",
"event_still_awaiting_approval": "Un evento è ancora in attesa della tua approvazione",
"booking_submitted_subject": "Prenotazione inviata: {{eventType}} con {{name}} il {{date}}",
"booking_submitted_subject": "Prenotazione inviata: {{title}} il {{date}}",
"your_meeting_has_been_booked": "Il tuo meeting è stata prenotato",
"event_type_has_been_rescheduled_on_time_date": "Il tuo {{title}} è stato riprogrammato per il {{date}}.",
"event_has_been_rescheduled": "Aggiornato: il tuo evento è stato riprogrammato",
@@ -420,10 +420,10 @@
"forgotten_secret_description": "Nel caso la parola segreta venga persa o dimenticata, è possibile cambiarla, ma tieni presente che tutte le integrazioni che la utilizzano dovranno essere aggiornate",
"current_incorrect_password": "La password attuale non è corretta",
"password_hint_caplow": "Combinazione di caratteri maiuscoli e minuscoli",
"password_hint_min": "La lunghezza minima è di 8 caratteri",
"password_hint_min": "La lunghezza minima è di 7 caratteri",
"password_hint_admin_min": "La lunghezza minima è di 15 caratteri",
"password_hint_num": "Deve contenere almeno 1 numero",
"invalid_password_hint": "La password deve avere una lunghezza minima di 7 caratteri e deve contenere almeno 1 numero e una combinazione di caratteri maiuscoli e minuscoli",
"invalid_password_hint": "La password deve avere una lunghezza minima di {{passwordLength}} caratteri e deve contenere almeno 1 numero e una combinazione di caratteri maiuscoli e minuscoli",
"incorrect_password": "La password non è corretta.",
"24_h": "24 ore",
"use_setting": "Usa impostazione",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} は、Calendly の代替となるオープンソースのツールです。データ、ワークフロー、外観などを自由にコントロールできます。",
"have_any_questions": "ご不明な点があれば、お気軽にお問い合わせください。",
"reset_password_subject": "{{appName}}: パスワードのリセット手順",
"event_declined_subject": "拒否されました: {{date}} の {{name}} との {{eventType}}",
"event_cancelled_subject": "キャンセルされました: {{date}} の {{name}} との {{eventType}}",
"event_declined_subject": "拒否されました: {{date}} の {{title}}",
"event_cancelled_subject": "キャンセルされました: {{date}} の {{title}}",
"event_request_declined": "イベントのリクエストが拒否されました",
"event_request_declined_recurring": "繰り返しイベントのリクエストが拒否されました",
"event_request_cancelled": "スケジュールされていた予定がキャンセルされました",
@@ -32,7 +32,7 @@
"refund_failed": "{{userName}} の {{date}} における {{eventType}} の返金に失敗しました。",
"check_with_provider_and_user": "これを処理する方法については、支払い業者と {{user}} にご確認ください。",
"a_refund_failed": "返金に失敗しました",
"awaiting_payment_subject": "お支払いを待機しています: {{date}} の {{name}} との {{eventType}}",
"awaiting_payment_subject": "お支払いを待機しています: {{date}} の {{title}}",
"meeting_awaiting_payment": "会議の支払いを待っています",
"help": "ヘルプ",
"price": "料金",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "別のユーザーがあなたのカレンダーにイベントを予定するリクエストしています。",
"someone_requested_password_reset": "別のユーザーがあなたのパスワードを変更するためのリンクをリクエストしています。",
"password_reset_instructions": "このリクエストをしていない場合は、このメールを無視しても大丈夫です。パスワードも変更されません。",
"event_awaiting_approval_subject": "承認を待っています: {{date}} の {{name}} との {{eventType}}",
"event_awaiting_approval_subject": "承認を待っています: {{date}} の {{title}}",
"event_still_awaiting_approval": "イベントはまだあなたの承認待ちです",
"booking_submitted_subject": "予約を送信しました: {{date}} の {{name}} との {{eventType}}",
"booking_submitted_subject": "予約を送信しました: {{date}} の {{title}}",
"your_meeting_has_been_booked": "ミーティングが予約されました",
"event_type_has_been_rescheduled_on_time_date": "あなたの {{title}} は {{date}} に再スケジュールされました。",
"event_has_been_rescheduled": "更新 - イベントのスケジュールが変更されました",
@@ -420,10 +420,10 @@
"forgotten_secret_description": "このシークレットを紛失または忘れた場合は変更が可能です。しかしこのシークレットを使っている連携はすべて最新のものにする必要があります",
"current_incorrect_password": "現在のパスワードが正しくありません",
"password_hint_caplow": "アルファベットの大文字と小文字を両方使用してください",
"password_hint_min": "8 文字以上入力してください",
"password_hint_min": "7 文字以上入力してください",
"password_hint_admin_min": "15 文字以上入力してください",
"password_hint_num": "少なくとも 1 文字は数字を含めてください",
"invalid_password_hint": "パスワードには少なくとも 1 文字以上数字を含め、アルファベットの大文字と小文字を両方使用した上で 7 文字以上の長さに設定してください",
"invalid_password_hint": "パスワードには少なくとも 1 文字以上数字を含め、アルファベットの大文字と小文字を両方使用した上で {{passwordLength}} 文字以上の長さに設定してください",
"incorrect_password": "パスワードが正しくありません。",
"24_h": "24時間",
"use_setting": "設定を使用",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}}은 오픈 소스 Calendly 대안으로 자신의 데이터, 워크플로 및 형태를 컨트롤할 수 있습니다.",
"have_any_questions": "질문이 있나요? 도와드릴게요.",
"reset_password_subject": "{{appName}}: 비밀번호 재설정 방법",
"event_declined_subject": "거절됨: {{date}} {{name}} 의 {{eventType}}",
"event_cancelled_subject": "취소됨: {{date}} {{name}} 의 {{eventType}}",
"event_declined_subject": "거절됨: {{date}} {{title}}",
"event_cancelled_subject": "취소됨: {{date}} {{title}}",
"event_request_declined": "회의 요청이 거절되었습니다.",
"event_request_declined_recurring": "되풀이 이벤트 요청이 거부되었습니다",
"event_request_cancelled": "회의 일정이 취소되었습니다.",
@@ -32,7 +32,7 @@
"refund_failed": "{{date}} {{userName}} 의 {{eventType}} 이벤트에 대한 환불이 이루어지지 않았습니다.",
"check_with_provider_and_user": "이 문제의 처리 방법은 결제 제공업체 및 {{user}}에서 확인하십시오.",
"a_refund_failed": "환불이 이루어지지 않았습니다.",
"awaiting_payment_subject": "결제를 기다리고 있습니다: {{date}} {{name}} 의 {{eventType}}",
"awaiting_payment_subject": "결제를 기다리고 있습니다: {{date}} {{title}}",
"meeting_awaiting_payment": "회의를 위해 결제해 주십시오.",
"help": "Help",
"price": "가격",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "누군가 귀하의 캘린더에 일정을 예약하도록 요청했습니다.",
"someone_requested_password_reset": "누군가 귀하의 비밀번호 변경을 위한 링크를 요청했습니다.",
"password_reset_instructions": "요청하지 않으셨다면 이 이메일을 무시하십시오. 비밀번호는 변경되지 않습니다.",
"event_awaiting_approval_subject": "승인을 기다리고 있습니다: {{date}} {{name}} 의 {{eventType}}",
"event_awaiting_approval_subject": "승인을 기다리고 있습니다: {{date}} {{title}}",
"event_still_awaiting_approval": "이벤트가 아직 승인을 기다리고 있습니다.",
"booking_submitted_subject": "예약 제출됨: {{date}}에 {{name}}에 의한 {{eventType}}",
"booking_submitted_subject": "예약 제출됨: {{date}}에 {{title}}",
"your_meeting_has_been_booked": "회의가 예약되었습니다.",
"event_type_has_been_rescheduled_on_time_date": "귀하의 {{title}} 일정이 {{date}}로 변경되었습니다.",
"event_has_been_rescheduled": "업데이트 - 이벤트 일정이 변경되었습니다.",
@@ -9,7 +9,7 @@
"calcom_explained": "{{appName}} is het open source Calendly-alternatief waarmee u uw eigen gegevens, werkstroom en uiterlijk in de hand houdt.",
"have_any_questions": "Heeft u vragen? We zijn er om te helpen.",
"reset_password_subject": "{{appName}}: Instructies voor het opnieuw instellen van uw wachtwoord",
"event_declined_subject": "Afgewezen: {{title}} op {{date}}",
"event_declined_subject": "Afgewezen: {{title}}",
"event_cancelled_subject": "Geannuleerd: {{title}} op {{date}}",
"event_request_declined": "Uw gebeurtenisverzoek is geweigerd",
"event_request_declined_recurring": "Uw verzoek voor een terugkerende gebeurtenis is geweigerd",
@@ -420,10 +420,10 @@
"forgotten_secret_description": "Als u dit geheim vergeten of verloren bent, kunt u het wijzigen. Maar wees u ervan bewust dat alle integraties die dit geheim gebruiken bijgewerkt moeten worden",
"current_incorrect_password": "Uw wachtwoord is verkeerd",
"password_hint_caplow": "Mix van hoofd- en kleine letters",
"password_hint_min": "Minimaal 8 tekens lang",
"password_hint_min": "Minimaal 7 tekens lang",
"password_hint_admin_min": "Minimaal 15 tekens lang",
"password_hint_num": "Minimaal 1 cijfer bevatten",
"invalid_password_hint": "Het wachtwoord moet minimaal 7 tekens lang zijn, minimaal 1 cijfer bevatten en bestaan uit een mix van hoofd- en kleine letters",
"invalid_password_hint": "Het wachtwoord moet minimaal {{passwordLength}} tekens lang zijn, minimaal 1 cijfer bevatten en bestaan uit een mix van hoofd- en kleine letters",
"incorrect_password": "Uw wachtwoord is incorrect.",
"24_h": "24u",
"use_setting": "Instelling gebruiken",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} er en åpen kildekodeversjon av Calendly som gir deg kontroll over dine egne data, arbeidsflyt og utseende.",
"have_any_questions": "Har du spørsmål? Vi er her for å hjelpe.",
"reset_password_subject": "{{appName}}: Instruksjoner for tilbakestilling av passord",
"event_declined_subject": "Avslått: {{eventType}} med {{name}} kl. {{date}}",
"event_cancelled_subject": "Avbrutt: {{eventType}} med {{name}} kl. {{date}}",
"event_declined_subject": "Avslått: {{title}} kl. {{date}}",
"event_cancelled_subject": "Avbrutt: {{title}} kl. {{date}}",
"event_request_declined": "Din hendelsesforespørsel har blitt avvist",
"event_request_declined_recurring": "Din gjentagende hendelsesforespørsel har blitt avvist",
"event_request_cancelled": "Din planlagte hendelse ble kansellert",
@@ -32,7 +32,7 @@
"refund_failed": "Tilbakebetalingen for hendelsen {{eventType}} med {{userName}} på {{date}} mislyktes.",
"check_with_provider_and_user": "Sjekk med betalingsleverandøren din og {{user}} hvordan du håndterer dette.",
"a_refund_failed": "En tilbakebetaling mislyktes",
"awaiting_payment_subject": "Venter på betaling: {{eventType}} med {{name}} {{date}}",
"awaiting_payment_subject": "Venter på betaling: {{title}} {{date}}",
"meeting_awaiting_payment": "Møtet ditt venter på betaling",
"help": "Hjelp",
"price": "Pris",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Noen har bedt om å planlegge en hendelse i kalenderen din.",
"someone_requested_password_reset": "Noen har bedt om en lenke for å endre passordet ditt.",
"password_reset_instructions": "Hvis du ikke har bedt om dette, kan du trygt ignorere denne e-posten og passordet ditt vil ikke bli endret.",
"event_awaiting_approval_subject": "Venter på godkjenning: {{eventType}} med {{name}} kl. {{date}}",
"event_awaiting_approval_subject": "Venter på godkjenning: {{title}} kl. {{date}}",
"event_still_awaiting_approval": "En hendelse venter fortsatt på din godkjenning",
"booking_submitted_subject": "Booking sendt inn: {{eventType}} med {{name}} kl. {{date}}",
"booking_submitted_subject": "Booking sendt inn: {{title}} kl. {{date}}",
"your_meeting_has_been_booked": "Møtet ditt er booket",
"event_type_has_been_rescheduled_on_time_date": "Din {{title}} har blitt flyttet til {{date}}.",
"event_has_been_rescheduled": "Oppdatert – Hendelsen din har blitt endret",
@@ -423,10 +423,10 @@
"forgotten_secret_description": "Hvis du har mistet eller glemt denne hemmeligheten, kan du endre den, men vær oppmerksom på at alle integrasjoner som bruker denne hemmeligheten må oppdateres",
"current_incorrect_password": "Nåværende passord er feil",
"password_hint_caplow": "Blanding av store og små bokstaver",
"password_hint_min": "Minimum 8 tegn langt",
"password_hint_min": "Minimum 7 tegn langt",
"password_hint_admin_min": "Minimum 15 tegn langt",
"password_hint_num": "Inneholde minst 1 tall",
"invalid_password_hint": "Passordet må være minst 7 tegn langt og inneholde minst ett tall og ha en blanding av store og små bokstaver",
"invalid_password_hint": "Passordet må være minst {{passwordLength}} tegn langt og inneholde minst ett tall og ha en blanding av store og små bokstaver",
"incorrect_password": "Passordet er feil.",
"24_h": "24t",
"use_setting": "Bruk innstilling",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} to alternatywa open source Calendly, która daje Ci kontrolę nad własnymi danymi, przepływem pracy i wyglądem.",
"have_any_questions": "Masz pytania? Jesteśmy tutaj, aby pomóc.",
"reset_password_subject": "{{appName}}: Instrukcje resetowania hasła",
"event_declined_subject": "Odrzucono: {{eventType}} z {{name}} w dniu {{date}}",
"event_cancelled_subject": "Anulowano: {{eventType}} z {{name}} w {{date}}",
"event_declined_subject": "Odrzucono: {{title}} w dniu {{date}}",
"event_cancelled_subject": "Anulowano: {{title}} w {{date}}",
"event_request_declined": "Twoja prośba o wydarzenie została odrzucona",
"event_request_declined_recurring": "Twoja prośba o wydarzenie cykliczne została odrzucona",
"event_request_cancelled": "Twoje zaplanowane wydarzenie zostało anulowane",
@@ -32,7 +32,7 @@
"refund_failed": "Zwrot środków za {{eventType}} z {{userName}} w {{date}} nie powiódł się.",
"check_with_provider_and_user": "Proszę, sprawdź z dostawcą płatności i {{user}} jak to zrobić.",
"a_refund_failed": "Zwrot środków nie powiódł się",
"awaiting_payment_subject": "Oczekiwanie na płatność: {{eventType}} z {{name}} w {{date}}",
"awaiting_payment_subject": "Oczekiwanie na płatność: {{title}} w {{date}}",
"meeting_awaiting_payment": "Twoje spotkanie oczekuje na płatność",
"help": "Pomoc",
"price": "Cena",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Ktoś poprosił o zaplanowanie wydarzenia w Twoim kalendarzu.",
"someone_requested_password_reset": "Ktoś poprosił o link do zmiany Twojego hasła.",
"password_reset_instructions": "Jeśli nie poprosiłeś o to, możesz bezpiecznie zignorować ten e-mail, a Twoje hasło nie zostanie zmienione.",
"event_awaiting_approval_subject": "Oczekiwanie na zatwierdzenie: {{eventType}} z {{name}} w {{date}}",
"event_awaiting_approval_subject": "Oczekiwanie na zatwierdzenie: {{title}} w {{date}}",
"event_still_awaiting_approval": "Wydarzenie nadal oczekuje na Twoje zatwierdzenie",
"booking_submitted_subject": "Rezerwacja potwierdzona: {{eventType}} z {{name}} w {{date}}",
"booking_submitted_subject": "Rezerwacja potwierdzona: {{title}} w {{date}}",
"your_meeting_has_been_booked": "Twoje spotkanie zostało zarezerwowane",
"event_type_has_been_rescheduled_on_time_date": "Twój {{title}} został przełożony na {{date}}.",
"event_has_been_rescheduled": "Zaktualizowano - Twoje wydarzenie zostało przełożone",
@@ -420,10 +420,10 @@
"forgotten_secret_description": "Jeśli nie pamiętasz tego wpisu tajnego lub nie masz do niego dostępu, możesz go zmienić, ale pamiętaj, że wszystkie integracje korzystające z tego wpisu tajnego będą musiały zostać zaktualizowane.",
"current_incorrect_password": "Bieżące hasło jest nieprawidłowe",
"password_hint_caplow": "Kombinacja wielkich i małych liter",
"password_hint_min": "Długość co najmniej 8 znaków",
"password_hint_min": "Długość co najmniej 7 znaków",
"password_hint_admin_min": "Długość co najmniej 15 znaków",
"password_hint_num": "Zawrzyj co najmniej 1 cyfrę",
"invalid_password_hint": "Hasło musi składać się z co najmniej 7 znaków, zawierać co najmniej jedną cyfrę oraz kombinację wielkich i małych liter",
"invalid_password_hint": "Hasło musi składać się z co najmniej {{passwordLength}} znaków, zawierać co najmniej jedną cyfrę oraz kombinację wielkich i małych liter",
"incorrect_password": "Hasło jest nieprawidłowe.",
"24_h": "24 godz.",
"use_setting": "Użyj ustawień",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} é a alternativa de código aberto ao Calendly que coloca você no controle dos seus dados, fluxo de trabalho e aparência.",
"have_any_questions": "Precisa de ajuda? Estamos aqui para ajudar.",
"reset_password_subject": "{{appName}}: Instruções para redefinir sua senha",
"event_declined_subject": "Recusado: {{eventType}} com {{name}} em {{date}}",
"event_cancelled_subject": "Cancelado: {{eventType}} com {{name}} em {{date}}",
"event_declined_subject": "Recusado: {{title}} em {{date}}",
"event_cancelled_subject": "Cancelado: {{title}} em {{date}}",
"event_request_declined": "Sua solicitação de reunião foi recusada",
"event_request_declined_recurring": "Sua solicitação de evento recorrente foi recusada",
"event_request_cancelled": "Seu evento agendado foi cancelado",
@@ -32,7 +32,7 @@
"refund_failed": "O reembolso do evento {{eventType}} com {{userName}} em {{date}} falhou.",
"check_with_provider_and_user": "Por favor, tente resolver junto do seu fornecedor de pagamentos e com o {{user}}.",
"a_refund_failed": "Um reembolso falhou",
"awaiting_payment_subject": "Aguardando pagamento: {{eventType}} com {{name}} em {{date}}",
"awaiting_payment_subject": "Aguardando pagamento: {{title}} em {{date}}",
"meeting_awaiting_payment": "Sua reunião está aguardando o pagamento",
"help": "Ajuda",
"price": "Preço",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Alguém solicitou o agendamento de um evento em sua agenda.",
"someone_requested_password_reset": "Alguém solicitou um link para alterar sua senha.",
"password_reset_instructions": "Se você não fez essa solicitação, você pode ignorar este e-mail e sua senha não será alterada.",
"event_awaiting_approval_subject": "Aguardando aprovação: {{eventType}} com {{name}} em {{date}}",
"event_awaiting_approval_subject": "Aguardando aprovação: {{title}} em {{date}}",
"event_still_awaiting_approval": "Um evento está aguardando a sua aprovação",
"booking_submitted_subject": "Reserva enviada: {{eventType}} com {{name}} em {{date}}",
"booking_submitted_subject": "Reserva enviada: {{title}} em {{date}}",
"your_meeting_has_been_booked": "A sua reunião foi agendada",
"event_type_has_been_rescheduled_on_time_date": "Seu {{title}} foi remarcado para {{date}}.",
"event_has_been_rescheduled": "Atualização - O seu evento foi reagendado",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} é a alternativa de código aberto de Calendly que lhe dá o controlo dos seus próprios dados, do seu fluxo de trabalho e da apresentação.",
"have_any_questions": "Tem perguntas? Estamos disponíveis para ajudar.",
"reset_password_subject": "{{appName}}: Instruções de redefinição da senha",
"event_declined_subject": "Declinado: {{eventType}} com {{name}} em {{date}}",
"event_cancelled_subject": "Cancelado: {{eventType}} com {{name}} em {{date}}",
"event_declined_subject": "Declinado: {{title}} em {{date}}",
"event_cancelled_subject": "Cancelado: {{title}} em {{date}}",
"event_request_declined": "O seu pedido de evento foi declinado",
"event_request_declined_recurring": "O seu pedido de evento recorrente foi recusado",
"event_request_cancelled": "O seu evento agendado foi cancelado",
@@ -32,7 +32,7 @@
"refund_failed": "Falhou o reembolso do evento {{eventType}} com {{userName}} em {{date}}.",
"check_with_provider_and_user": "Por favor, tente resolver junto do seu fornecedor de pagamentos e com {{userName}}.",
"a_refund_failed": "Falhou ao reembolsar",
"awaiting_payment_subject": "A aguardar pagamento: {{eventType}} com {{name}} em {{date}}",
"awaiting_payment_subject": "A aguardar pagamento: {{title}} em {{date}}",
"meeting_awaiting_payment": "A sua reunião aguarda pagamento",
"help": "Ajuda",
"price": "Preço",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Alguém pediu para agendar um evento no seu calendário.",
"someone_requested_password_reset": "Alguém pediu uma ligação para alterar a sua senha.",
"password_reset_instructions": "Se não fez este pedido, pode ignorar este email com segurança e a sua senha não será alterada.",
"event_awaiting_approval_subject": "A aguardar aprovação: {{eventType}} com {{name}} em {{date}}",
"event_awaiting_approval_subject": "A aguardar aprovação: {{title}} em {{date}}",
"event_still_awaiting_approval": "Um evento ainda aguarda a sua aprovação",
"booking_submitted_subject": "Reserva submetida: {{eventType}} com {{name}} em {{date}}",
"booking_submitted_subject": "Reserva submetida: {{title}} em {{date}}",
"your_meeting_has_been_booked": "A sua reunião foi reservada",
"event_type_has_been_rescheduled_on_time_date": "Seu {{title}} foi remarcado para {{date}}.",
"event_has_been_rescheduled": "O seu evento foi reagendado.",
@@ -423,7 +423,7 @@
"password_hint_min": "Tamanho mínimo de 7 caracteres",
"password_hint_admin_min": "Tamanho mínimo de 15 caracteres",
"password_hint_num": "Contém pelo menos 1 número",
"invalid_password_hint": "A palavra-passe deve ter no mínimo 7 caracteres, com pelo menos um número e uma mistura de letras maiúsculas e minúsculas",
"invalid_password_hint": "A palavra-passe deve ter no mínimo {{passwordLength}} caracteres, com pelo menos um número e uma mistura de letras maiúsculas e minúsculas",
"incorrect_password": "Palavra-Passe incorreta",
"24_h": "24h",
"use_setting": "Usar Configuração",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} este alternativa open-source la Calendly, care vă oferă control asupra propriilor date, asupra fluxului de lucru și asupra aspectului site-ului.",
"have_any_questions": "Aveți întrebări? Suntem aici pentru a vă ajuta.",
"reset_password_subject": "{{appName}}: instrucțiuni de resetare a parolei",
"event_declined_subject": "Refuzat: {{eventType}} cu {{name}} în {{date}}",
"event_cancelled_subject": "Anulat: {{eventType}} cu {{name}} în {{date}}",
"event_declined_subject": "Refuzat: {{title}} în {{date}}",
"event_cancelled_subject": "Anulat: {{title}} în {{date}}",
"event_request_declined": "Solicitarea pentru eveniment a fost refuzată",
"event_request_declined_recurring": "Solicitarea dvs. pentru evenimentul recurent a fost refuzată",
"event_request_cancelled": "Evenimentul programat a fost anulat",
@@ -32,7 +32,7 @@
"refund_failed": "Rambursarea pentru eveniment {{eventType}} cu {{userName}} în {{date}} a eșuat.",
"check_with_provider_and_user": "Vă rugăm să verificați cu furnizorul dvs. de plăți și cu {{userName}} cum să gestionați această activitate.",
"a_refund_failed": "Rambursare eșuată",
"awaiting_payment_subject": "Se așteaptă plata: {{eventType}} cu {{name}} în {{date}}",
"awaiting_payment_subject": "Se așteaptă plata: {{title}} în {{date}}",
"meeting_awaiting_payment": "Întâlnirea dvs. așteaptă plata",
"help": "Ajutor",
"price": "Prețul",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Cineva a solicitat să programeze un eveniment în calendar.",
"someone_requested_password_reset": "Cineva a solicitat un link pentru a vă schimba parola.",
"password_reset_instructions": "Dacă nu ați solicitat acest lucru, puteți ignora acest e-mail, iar parola nu va fi schimbată.",
"event_awaiting_approval_subject": "Se așteaptă aprobarea: {{eventType}} cu {{name}} în {{date}}",
"event_awaiting_approval_subject": "Se așteaptă aprobarea: {{title}} în {{date}}",
"event_still_awaiting_approval": "Un eveniment încă așteaptă aprobarea dvs.",
"booking_submitted_subject": "Rezervare transmisă: {{eventType}} cu {{name}} în {{date}}",
"booking_submitted_subject": "Rezervare transmisă: {{title}} în {{date}}",
"your_meeting_has_been_booked": "Întâlnirea dvs. a fost programată",
"event_type_has_been_rescheduled_on_time_date": "{{title}} dvs. a fost reprogramat la {{date}}.",
"event_has_been_rescheduled": "Evenimentul dvs. a fost reprogramat.",
@@ -420,10 +420,10 @@
"forgotten_secret_description": "Dacă ați pierdut sau ați uitat acest secret, îl puteți modifica, dar rețineți că toate integrările cu acest secret vor trebui actualizate",
"current_incorrect_password": "Parola curentă este incorectă",
"password_hint_caplow": "Combinație de litere mari și litere mici",
"password_hint_min": "Cel puțin 8 caractere",
"password_hint_min": "Cel puțin 7 caractere",
"password_hint_admin_min": "Cel puțin 15 caractere",
"password_hint_num": "Conține cel puțin 1 cifră",
"invalid_password_hint": "Parola trebuie să aibă cel puțin 7 caractere, să conțină cel puțin o cifră și o combinație de litere mari și litere mici",
"invalid_password_hint": "Parola trebuie să aibă cel puțin {{passwordLength}} caractere, să conțină cel puțin o cifră și o combinație de litere mari și litere mici",
"incorrect_password": "Parola este incorectă.",
"24_h": "24h",
"use_setting": "Folosește setarea",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} - это альтернатива Calendly с открытым исходным кодом, позволяющая контролировать собственные данные, рабочий процесс и внешний вид.",
"have_any_questions": "Есть вопросы? Мы здесь, чтобы помочь.",
"reset_password_subject": "{{appName}}: Инструкция по сбросу пароля",
"event_declined_subject": "Отклонено: {{eventType}} с {{name}} на {{date}}",
"event_cancelled_subject": "Отменено: {{eventType}} с {{name}} на {{date}}",
"event_declined_subject": "Отклонено: {{title}} на {{date}}",
"event_cancelled_subject": "Отменено:{{title}} на {{date}}",
"event_request_declined": "Ваш запрос на встречу отклонен",
"event_request_declined_recurring": "Ваш запрос на повторяющееся событие отклонен",
"event_request_cancelled": "Ваша запланированная встреча была отменена",
@@ -32,7 +32,7 @@
"refund_failed": "Возврат за встречу {{eventType}} с {{userName}} на {{date}} не удался.",
"check_with_provider_and_user": "Пожалуйста, проверьте в вашей платежной системе и {{userName}} как это сделать.",
"a_refund_failed": "Возврат не удался",
"awaiting_payment_subject": "Ожидание оплаты: {{eventType}} с {{name}} на {{date}}",
"awaiting_payment_subject": "Ожидание оплаты: {{title}} на {{date}}",
"meeting_awaiting_payment": "Ваша встреча ожидает оплаты",
"help": "Помощь",
"price": "Цена",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Кто-то записался на встречу в вашем календаре.",
"someone_requested_password_reset": "Кто-то запросил ссылку для изменения вашего пароля.",
"password_reset_instructions": "Если вы не запрашивали это, проигнорируйте это письмо и пароль не будет изменен.",
"event_awaiting_approval_subject": "Ожидает подтверждения: {{eventType}} с {{name}} на {{date}}",
"event_awaiting_approval_subject": "Ожидает подтверждения: {{title}} на {{date}}",
"event_still_awaiting_approval": "Событие все еще ждет вашего подтверждения",
"booking_submitted_subject": "Бронирование отправлено: {{eventType}} с {{name}}, {{date}}",
"booking_submitted_subject": "Бронирование отправлено: {{title}}, {{date}}",
"your_meeting_has_been_booked": "Ваша встреча была забронирована",
"event_type_has_been_rescheduled_on_time_date": "Ваше {{title}} было перенесено на {{date}}.",
"event_has_been_rescheduled": "Ваше событие было перенесено.",
@@ -423,7 +423,7 @@
"password_hint_min": "Не короче 7 символов",
"password_hint_admin_min": "Не короче 15 символов",
"password_hint_num": "Не менее 1 цифры",
"invalid_password_hint": "Пароль должен быть не короче 7 символов и содержать заглавные и строчные буквы, а также не менее одной цифры",
"invalid_password_hint": "Пароль должен быть не короче {{passwordLength}} символов и содержать заглавные и строчные буквы, а также не менее одной цифры",
"incorrect_password": "Неверный пароль",
"24_h": "24 часа",
"use_setting": "Использовать настройки",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} je Calendly alternativa sa otvorenim kodom koja vam omogućava kontrolu nad vašim podacima, procesom rada i izgledom.",
"have_any_questions": "Imate pitanja? Tu smo da pomognemo.",
"reset_password_subject": "{{appName}}: Upustva za resetovanje lozinke",
"event_declined_subject": "Odbijeno: {{eventType}} sa {{name}} datuma {{date}}",
"event_cancelled_subject": "Otkazano: {{eventType}} sa {{name}} datuma {{date}}",
"event_declined_subject": "Odbijeno: {{title}} datuma {{date}}",
"event_cancelled_subject": "Otkazano: {{title}} datuma {{date}}",
"event_request_declined": "Vaš zahtev za dogadjaj je odbijen",
"event_request_declined_recurring": "Vaš zahtev za ponavljajući događaj je odbijen",
"event_request_cancelled": "Vaš zakazan dogadjaj je odbijen",
@@ -32,7 +32,7 @@
"refund_failed": "Povraćaj novca za dogadjaj {{eventType}} sa {{userName}} datuma {{date}} je neuspešan.",
"check_with_provider_and_user": "Molimo vas proverite sa vašom uslugom plaćanja i {{user}} kako bi rešili ovo.",
"a_refund_failed": "Povraćaj novca neuspešan",
"awaiting_payment_subject": "Čekanje na uplatu: {{eventType}} sa {{name}} datuma {{date}}",
"awaiting_payment_subject": "Čekanje na uplatu: {{title}} datuma {{date}}",
"meeting_awaiting_payment": "Vaš sastanak čeka na uplatu",
"help": "Pomoć",
"price": "Cena",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Neko zahteva da rezerviše dogadjaj na vašem kalendaru.",
"someone_requested_password_reset": "Neko zahteva link da promeni vašu lozinku.",
"password_reset_instructions": "Ako niste pokrenuli ovaj zahtev, slobodno ignorišite ovu e-poštu i vaša lozinka se neće promeniti.",
"event_awaiting_approval_subject": "Čeka na odobrenje: {{eventType}} sa {{name}} datuma {{date}}",
"event_awaiting_approval_subject": "Čeka na odobrenje: {{title}} datuma {{date}}",
"event_still_awaiting_approval": "Događaj još čeka na vaše odobrenje",
"booking_submitted_subject": "Rezervacija podneta: {{eventType}} sa {{name}} datuma {{date}}",
"booking_submitted_subject": "Rezervacija podneta:{{title}} datuma {{date}}",
"your_meeting_has_been_booked": "Sastanak je zakazan",
"event_type_has_been_rescheduled_on_time_date": "Ваш {{титле}} је померен за {{дате}}.",
"event_has_been_rescheduled": "Ažurirano - Vaš dogadjaj je odložen",
@@ -423,7 +423,7 @@
"password_hint_min": "Dugačka minimalno 7 znakova",
"password_hint_admin_min": "Dugačka minimalno 15 znakova",
"password_hint_num": "Sadrži barem 1 broj",
"invalid_password_hint": "Lozinka mora da bude dugačka najmanje 7 znakova, da sadrži najmanje jedan broj i da ima mešavinu velikih i malih slova",
"invalid_password_hint": "Lozinka mora da bude dugačka najmanje {{passwordLength}} znakova, da sadrži najmanje jedan broj i da ima mešavinu velikih i malih slova",
"incorrect_password": "Lozinka je netačna.",
"24_h": "24 sata",
"use_setting": "Koristi podešavanje",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} är öppen källkods-alternativet till Calendly som ger dig kontroll över din egen data, ditt arbetsflöde och ditt utseende.",
"have_any_questions": "Har du frågor? Vi är här för att hjälpa till.",
"reset_password_subject": "{{appName}}: Instruktioner för att återställa ditt lösenord",
"event_declined_subject": "Avböjd: {{eventType}} med {{name}} vid {{date}}",
"event_cancelled_subject": "Inställd: {{eventType}} med {{name}} vid {{date}}",
"event_declined_subject": "Avböjd: {{title}} vid {{date}}",
"event_cancelled_subject": "Inställd: {{title}} vid {{date}}",
"event_request_declined": "Din bokningsförfrågan har avbjöts",
"event_request_declined_recurring": "Din återkommande händelse har avböjts",
"event_request_cancelled": "Din schemalagda bokning ställdes in",
@@ -32,7 +32,7 @@
"refund_failed": "Återbetalningen för bokningen {{eventType}} med {{userName}} den {{date}} misslyckades.",
"check_with_provider_and_user": "Vänligen kontrollera med din betalningsleverantör och {{user}} hur du hanterar detta.",
"a_refund_failed": "Återbetalning misslyckades",
"awaiting_payment_subject": "Inväntar betalning: {{eventType}} med {{name}} på {{date}}",
"awaiting_payment_subject": "Inväntar betalning: {{title}} på {{date}}",
"meeting_awaiting_payment": "Din bokning väntar på betalning",
"help": "Hjälp",
"price": "Pris",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Någon har begärt att schemalägga en händelse i din kalender.",
"someone_requested_password_reset": "Någon har begärt en länk för att ändra ditt lösenord.",
"password_reset_instructions": "Om du inte begärde detta kan du ignorera detta e-postmeddelande och ditt lösenord kommer inte att ändras.",
"event_awaiting_approval_subject": "Inväntar godkännande: {{eventType}} med {{name}} den {{date}}",
"event_awaiting_approval_subject": "Inväntar godkännande: {{title}} den {{date}}",
"event_still_awaiting_approval": "En bokning väntar fortfarande på ditt godkännande",
"booking_submitted_subject": "Bokning skickad: {{eventType}} med {{name}} {{date}}",
"booking_submitted_subject": "Bokning skickad: {{title}} {{date}}",
"your_meeting_has_been_booked": "Ditt möte har bokats",
"event_type_has_been_rescheduled_on_time_date": "Din {{title}} har ändrats till {{date}}.",
"event_has_been_rescheduled": "Uppdatering - Din bokning har blivit omplanerad",
@@ -420,10 +420,10 @@
"forgotten_secret_description": "Om du tappat bort eller glömt den här hemligheten kan du ändra den, men var medveten om att alla integrationer som använder hemligheten måste uppdateras",
"current_incorrect_password": "Nuvarande lösenord är felaktigt",
"password_hint_caplow": "Blandning av versaler och gemener",
"password_hint_min": "Minst 8 tecken",
"password_hint_min": "Minst 7 tecken",
"password_hint_admin_min": "Minst 15 tecken",
"password_hint_num": "Innehåller minst 1 siffra",
"invalid_password_hint": "Lösenordet måste innehålla minst 7 tecken och har minst en siffra samt en blandning av versaler och gemener",
"invalid_password_hint": "Lösenordet måste innehålla minst {{passwordLength}} tecken och har minst en siffra samt en blandning av versaler och gemener",
"incorrect_password": "Lösenordet är felaktigt.",
"24_h": "24 timmar",
"use_setting": "Använd inställning",
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}}, kendi verilerinizin, iş akışınızın ve web görünümünüzün kontrolünü size veren açık kaynaklı Calendly uygulamasının alternatifidir.",
"have_any_questions": "Sorularınız mı var? Size yardımcı olmak için buradayız.",
"reset_password_subject": "{{appName}}: Şifre sıfırlama talimatları",
"event_declined_subject": "Reddedildi: {{date}} tarihinde {{name}} ile {{eventType}}",
"event_cancelled_subject": "İptal edildi: {{date}} tarihinde {{name}} ile {{eventType}}",
"event_declined_subject": "Reddedildi: {{date}} tarihinde {{title}}",
"event_cancelled_subject": "İptal edildi: {{date}} tarihinde {{title}}",
"event_request_declined": "Etkinlik talebiniz reddedildi",
"event_request_declined_recurring": "Yinelenen etkinlik talebiniz reddedildi",
"event_request_cancelled": "Planlanan etkinliğiniz iptal edildi",
@@ -32,7 +32,7 @@
"refund_failed": "{{date}} tarihindeki {{userName}} ile {{eventType}} etkinliğinin iadesi başarısız oldu.",
"check_with_provider_and_user": "Lütfen ödeme sağlayıcınızla ve {{user}} ile bunu nasıl ele alınacağınızı kontrol edin.",
"a_refund_failed": "İade başarısız oldu",
"awaiting_payment_subject": "Ödeme Bekleniyor: {{date}} tarihinde {{name}} ile {{eventType}}",
"awaiting_payment_subject": "Ödeme Bekleniyor: {{date}} tarihinde {{title}}",
"meeting_awaiting_payment": "Toplantınız için ödeme bekleniyor",
"help": "Yardım",
"price": "Fiyat",
@@ -63,9 +63,9 @@
"someone_requested_an_event": "Birisi takviminizde bir etkinlik planlamak istiyor.",
"someone_requested_password_reset": "Biri şifrenizi değiştirmek için bir bağlantı talebinde bulundu.",
"password_reset_instructions": "Böyle bir talepte bulunmadıysanız bu e-postayı güvenle yok sayabilirsiniz. Şifreniz değiştirilmeyecektir.",
"event_awaiting_approval_subject": "Onay Bekleniyor: {{date}} tarihinde {{name}} ile {{eventType}}",
"event_awaiting_approval_subject": "Onay Bekleniyor: {{date}} tarihinde {{title}}",
"event_still_awaiting_approval": "Bir etkinlik hâlâ onayınızı bekliyor",
"booking_submitted_subject": "Rezervasyon Gönderildi: {{date}} tarihinde {{name}} ile {{eventType}}",
"booking_submitted_subject": "Rezervasyon Gönderildi: {{date}} tarihinde {{title}}",
"your_meeting_has_been_booked": "Toplantı rezervasyonunuz yapıldı",
"event_type_has_been_rescheduled_on_time_date": "{{title}}, {{date}} olarak yeniden planlandı.",
"event_has_been_rescheduled": "Güncellendi - Etkinliğiniz yeniden planlandı",
@@ -420,10 +420,10 @@
"forgotten_secret_description": "Bu gizli anahtarı kaybettiyseniz veya unuttuysanız değiştirebilirsiniz ancak bu gizli anahtarı kullanan tüm entegrasyonların güncellenmesi gerekecektir",
"current_incorrect_password": "Mevcut şifre hatalı",
"password_hint_caplow": "Büyük ve küçük harf karışımı",
"password_hint_min": "En az 8 karakter uzunluğunda",
"password_hint_min": "En az 7 karakter uzunluğunda",
"password_hint_admin_min": "Minimum uzunluk 15 karakter",
"password_hint_num": "En az 1 rakam içermelidir",
"invalid_password_hint": "Şifre en az 7 karakter uzunluğunda olmalı, en az bir rakam içermeli ve büyük ve küçük harflerin karışımından oluşmalıdır",
"invalid_password_hint": "Şifre en az {{passwordLength}} karakter uzunluğunda olmalı, en az bir rakam içermeli ve büyük ve küçük harflerin karışımından oluşmalıdır",
"incorrect_password": "Şifre hatalı.",
"24_h": "24 sa",
"use_setting": "Ayarı kullan",

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