Compare commits

...
55 Commits
Author SHA1 Message Date
Alex van Andel 5560f9def7 v2.6.12
release.yml / release (push) Canceled after 0s
2023-03-11 20:10:51 +00:00
Alex van Andel 14c38f5108 Small fix to next-auth-options (#7674) 2023-03-11 20:04:42 +00:00
229aeeb0c9 Fix booking time hydration error (#7253)
Root cause of the error is that both server and client try to parse date differently due to localStorage. Since server does not know localStorage when render initial HTML it renders page as if there is no value set for time format.
 On the other hand, client side know the value of time format when it tries to render.

To fix the issue, we force it to parse date with default format at the initial rendering time.

- Add `withDefaultTimeFormat` argument to `parseDate` function
- Add `withDefaultTimeFormat` argument to `parseRecurringDates` function
- Leverage useEffect to dynamically update `withDefaultTimeFormat` arg passed to function

Co-authored-by: sangboak.lee <sangboak.lee@woowahan.com>
Co-authored-by: zomars <zomars@me.com>
2023-03-11 00:24:01 +00:00
Omar López 6a9704349d Revert "Fix conflicts for embed tests (#7311)" (#7670)
This reverts commit 8b47c064ff.
2023-03-10 16:59:58 -07:00
github-actions[bot]andCrowdin Bot d2ffa08c83 New Crowdin translations by Github Action (#7632)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-03-10 16:55:21 -07:00
Lucas Smithandzomars 2317473545 feat(web): improve session retrieval performance (#7584)
* feat(web): improve session retrieval performance

Switch to using `getServerSession` which avoids a HTTP round trip to retrieve session details.

Additionally, migrate deprecated `app/lib/auth` calls to to `@calcom/lib` package.

* fix: update failing test and lint

* Consolidates auth code in features

* Update yarn.lock

* Update packages/trpc/server/createContext.ts

* Oopsie

---------

Co-authored-by: zomars <zomars@me.com>
2023-03-10 23:45:24 +00:00
Lucas SmithandOmar López d81d772cdf feat(lib): add more tests to lib package (#7210)
* feat(lib): add more tests to lib package

Add more tests to the lib package to make it more robust overall. Additionally, tidy any methods that can be modified without changing behaviour and tighten types where possible.

* fix(lib): update missed imports

* fix: revert stylistic changes

* Update getSchedule.test.ts

---------

Co-authored-by: Omar López <zomars@me.com>
2023-03-10 22:10:56 +00:00
cbc9cd60d5 [CAL-988] Limit total appointment time per day/week/month/year (#7166)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-03-10 21:00:19 +01:00
+24 eb66931366 fix(calendar): recurring event fix (#6959)
* 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)

* fix(calendar): recurring event fix

the loop was running way too often on every recurring object
mostly due to the fact that the iterator was not checked for finalization,
and in fact the iterator is not implemented correctly so it does not have done.
instead it checks for undefined or not

Signed-off-by: Tobias Gurtzick <magic@wizardtales.com>

* fix(calendar): recurring started in stoneage
add a start date to the iterator creation

Signed-off-by: Tobias Gurtzick <magic@wizardtales.com>

* fix(calendar): no reason for do here

Signed-off-by: Tobias Gurtzick <magic@wizardtales.com>

* fix(calendar): hour must contain same values as date calendar object...

Signed-off-by: Tobias Gurtzick <magic@wizardtales.com>

* Update yarn.lock

* Adds deployment settings to DB (#6706)

* WIP: SIU: Maximum call stack size exceeded fix? (#6740)

* Maximum call stack size exceeded fix?

# Conflicts:
#	packages/prisma/index.ts

* Prisma client extensions is too much for our TS

* Removed unused Icons

* Type fixes

* replaced edit location dialog subtitle with link to app store (#6746)

* Upgrades typescript to 4.9.4 (#6747)

* Upgrades typescript to 4.9.4

* Type fixes

* Adds booking data to SDK success event (#6737)

* On duplicating an event type, spacing between arrows and label is wide (Length field) (#6717)

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

* Update README.md

* Update README.md

* added blue color for links in bio (#6760)

* removed credits from auth container (#6766)

* On seats, hide attendees if option is selected (#6767)

* On seats, hide attendees if option is selected

* Remove unused line

* Fix shift-enter in bio editor (#6769)

* fix wrong enabled update button at first load

* fix shift-enter error

* small fixes + code clean up

---------

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

* fix: back button and heading size (#6768)

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

* Disable autocomplete on password field (#6705)

* fix padding of lenght input on add event modal (#6771)

* Fix: EventType Duplication (#6778)

* fix: location select menu overflowing the container (#6719)

* Update README.md

* adding ukrainian (#6791)

* fix(web): set canonical as `https://cal.com` in the `<HeadSeo/>` (#6693)

* fix(web): set canonical as https://cal.com in the <HeadSeo/>

* use `window.document.location?.href` if self hosted

* clean up

* use `isCalcom` from library

* handle trailing slash on homepage again

* added impersonation tip (#6794)

* Standardize response codes in Login (#6787)

* standardizing login response codes

* playwright fix

* added translation

* fix: booking limits saved as undefined (#6720)

* fix: booking limits saved as undefined

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

* fix: add event limit

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

---------

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

* Correct invalid ukraine language code ua -> uk

* fix(web): remove `noindex` & `nofollow` tags from the `App` and `Shell` components (#6816)

Fixes https://github.com/calcom/cal.com/issues/6815

* fix: border color in dark teams page (#6817)

* Add additional guests (#6419)

* Add additional guests

* WIP

* WIP

* Type fix

* WIP

* Add validation for already invited attendees

* Revert type to email

* remove controlled fields

* disables signups via env variable (#6212)

* disables signups via env variable

* Apply suggestions from code review

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

* Reusable Upgrade component for teams only features (#6473)

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

* i18n udpate

* fix: darkmode in booking/[id] page (#6458)

* fix: text-area style

* fix: border styles

* fix: german translation for reschedule booking email  subject (#6483)

* fix: german translation for reschedule subject

* fix: add a full stop

* fix: app store spacing issues (#6481)

* feat: add equal spacing with gap

* fix: title style

* fix: title style

* fix: remove margin

* fix: remove margin

* fix: sub header spacing and slider button

* fix: category card text

* fix: app store spacing issues

* fix: tab in tablet

* fix: shell

* fix: styling of dropdown component (#6440)

* fix: styling of dropdown component

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

* Apply suggestions from code review

* fix: use dropdown item in all pages

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

* chore: undo

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>

* readding sidebar tips for everyone, added 3 more features to teams empty screen (#6475)

* readding sidebar tips for everyone

* added 3 more features to teams plan

* responsive fixes

* responsive fixes

* small responsive fix

* CTA alignment fixed

* Minor alignment fixes to Shell for event-types/[type]

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

* fix: wrong i18n  placeholder in many locales (#6496)

* fix: czech translation

* fix: spanish translation

* fix: italian translation

* fix: japanese translation

* fix: korean translation

* fix: dutch translation

* fix: norwegian translation

* fix: polish translation

* fix: portuguese translation

* fix: portuguese translation

* fix: romanian translation

* fix: russian translation

* fix: serbian translation

* fix: swedish translation

* fix: turkish translation

* fix: ukrainian translation

* fix: vietnamese translation

* fix: chinese simplified translation

* fix: chinese traditional translation

* Disable redirect for cancellation page (#6498)

* [CAL-677] /event-type : Event Setup sidebar item doesn't reflect multiple durations (#6390)

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

* Added fallback to hostsFixed too which is used in COLLECTIVE (#6500)

* Don't update the field when changing the select (#6486)

* Don't update the field when changing the select

* Fixing missing label

* Applied the correct y-8 vertical margin

* Changed the description vertical offset from title a bit more

* Add Trash icon on Availability list dropdown

* fix: tablet sidebar profile being too wide and not centered  (#6490)

* feat: updated app list items styling issues inside settings/conferencing

* feat: fixed tablet sidebar width and not centered issue

* fix: removed un-related changes from this pr

* deleted .env

* fix: added the accidentaly deleted .env file

* fix: removed unwanted .env file from the pr

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* Bring back PublicShell (#6505)

* Add red text to error message

* WIP

* Rearrange guest input

* Fix validate unique guests logic

* Allow guests on reschedule

* Lint fix

* More lint fixes

* Lint fixes

* More lint fixes

* Fix remove guest button margin

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Jeff Loiselle <jeff@loiselles.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.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: Hariom Balhara <hariombalhara@gmail.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: Ronit Panda <72537293+rtpa25@users.noreply.github.com>
Co-authored-by: Alan <alannnc@gmail.com>

* fix: flash of i18n in teams page (#6818)

* Change lint action to run on BuildJet (#6819)

* Feat - invite user to team flow improvements (#6725)

* Fix Dutch custom event names (#6806)

Continued from #6375

* [CAL-727] [500] on /api/trpc/[trpc] (#6795)

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

* update isCalcom for canonicals to handle https://cal.com (#6802)

* update isCalcom for canonicals to handle https://cal.com

* avoid env var

* prettier

* handle window undefined

* handle window undefined during pre-render

* add some logs

* add some logs

* Revert "add some logs"

This reverts commit 9d27de029024e526417bc79e9cee42d7333ac592.

* change useHasTeamPlan hook to useHasPaidPlan (#6699)

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

* fix: use guest email in create booking link banner (#6694)

* feat: tabs for teams in mobile  (#6685)

* fix: padding

* feat: add teams tab component

* fix: add image to type

* fix: current path check

* fix: add avatar prop

* feat: add teams tab for mobile

* fix: padding

* fix: background and padding x

* fix: empty logic

* fix: conditions

* fix: bg

---------

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

* [CAL-807] /event-types/single - Fix the header (#6738)

* [CAL-807] /event-types/single - Fix the header

* [CAL-807] /event-types/single - Fix the header

* [CAL-807] /event-types/single - Fix the header

---------

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

* Session Timeout (#6439)

* Implementation

* Unifying UI to password screen

* Tweaks for session in password section

* Update apps/web/pages/settings/security/password.tsx

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

* Update packages/features/kbar/Kbar.tsx

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

* Update packages/features/settings/layouts/SettingsLayout.tsx

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

* Relying on extra db query

* 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

* Using same version as other deps

* Reverting prisma changes and fixing things

* Uneeded tx-expect-error

* Fixing default value

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

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* Fix embed modal leading inputs (#6822)

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

* Hide attendees from calendar description (#6800)

* Show the organizer timezone when booking on seats

* Only show organizer in event description

* Type fixes

* Update snapshot with new response

---------

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

* fix: installed apps and admin apps layout spacing issues. (#6753)

* feat: add highlight props

* feat: add isTemplate prop

* feat: add invalid credintial prop

* feat: add children prop

* feat: render app list card

* fix: add horizontal gap

* fix: installed app layout style

* fix: admin layout

* feat: add isGlobal to returned data

* feat: fix admin tab.

* use common components list and AppListCard

* show isDefault Badge

* hide switch for global apps

* fix: show switch

* fix: remove isglobal

* fix: layout

* refactor: remove unused component and import

* feat: use app list card component

* fix: query param

* fix: prevent unnecessary props passed to the component

* feat: add disabled style

* feat: add disconnect integration modal

* feat: new changes.

* render disconnect integration model only once.

* add dropdown for actions

* feat: changes in admin apps.

* use dropdown for selecting action buttons.

* use modal for editing keys.

* Remove boolean comparison

---------

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

* user management moved to console.cal.com (#6799)

* Removed unused variables (#6704) (#6750)

* re-adding subtitle to Shell

* lint

* fixed event-type description

* fix: input component style in routing forms (#6731)

* fix: remove custom className

* removed extra type text

removed extra type text from input

---------

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

* Add X-Frame-Options to page headers (#6784)

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

* Removes pay_later_instructions (#6824)

* [CAL-845] Improve the custom event name modal UI/UX (#6660)

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

* webhook from ui design change (#6803)

* webhook from ui change

* fix: webhook test border & test webhook font style

* Fixed broken path to fix SB build failure

* feat: add theming options to teams (#6807)

* /teams improvements (#6831)

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

* Invalidate hasTeamPlan when the last team is removed (#6837)

* Not allowing random duration (#6743)

* v2.5.7

* fix: padding in shell for mobile (#6825)

* fix: padding

* fix: breakpoints in fab

* fix: padding

* Move e2e to buildjet (#6840)

* i18n fix

* i18n fix

* [CAL-713] /availability single - layout issues. Check spacing, margin, alignment with spec'd design (#6724)

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

* fix: make cal video default value (#6716)

* [CAL-90] Event Types Single / Allow multiple physical locations (#6692)

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

* Don't override but decorate the built-in encode function (#6850)

* Don't override but decorate the built-in encode function

* Removed jose + synced lockfile

* Give highest possible value to z-index (#6853)

* Use trackingId only for trackingDomain as thats the only variable supported (#6856)

* v2.5.8

* fixed availabilty layout

* Left / Right buttons now bottom-aligned and slightly shorter (#6857)

Fixes https://github.com/calcom/cal.com/issues/6589

* 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

* Update RadioAreaGroup.tsx

* Update CalendarService.ts

* Update CalendarService.ts

---------

Signed-off-by: Tobias Gurtzick <magic@wizardtales.com>
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Tobias Gurtzick <magic@wizardtales.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.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: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Sai Deepesh <saideepesh000@gmail.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: Richard Poelderl <richard.poelderl@gmail.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Jeff Loiselle <jeff@loiselles.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Ronit Panda <72537293+rtpa25@users.noreply.github.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Bram Verstraten <bramverstraten@outlook.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Ben Lam <37053985+blam404@users.noreply.github.com>
Co-authored-by: Josh Avant <joshavant@gmail.com>
Co-authored-by: Joe Shajan <joeshajan1551@gmail.com>
Co-authored-by: Aaron Presley <155617+AaronPresley@users.noreply.github.com>
Co-authored-by: Ben Hybert <53020786+Hybes@users.noreply.github.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: Deepak Prabhakara <deepak@boxyhq.com>
2023-03-10 10:58:26 -07:00
Alex van Andel 164f8918f5 v2.6.11
release.yml / release (push) Canceled after 0s
2023-03-10 16:54:51 +00:00
Joe Au-Yeung 7b21839c73 Save refresh token (#7661) 2023-03-10 16:42:20 +00:00
Keith Williams f3360e6476 Fix jackson build error (#7641)
* Fixing prettier build issue with jackson

* Refactor to clean up code

* Now using globalThis to reference jackson objects
2023-03-10 15:28:57 +00:00
Nafees Nazik e24f1d8bdb feat: translate attendee scheduled email in bookers locale (#7654) 2023-03-10 14:38:29 +00:00
Carina WollendorferandCarinaWolli d50473468a Delay workflow email cancellation in sendgrid for all case (#7647)
* don't use immediate email cancellation in any case

* use continue

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-03-10 14:28:42 +00:00
Joe Au-Yeung 4e8653cf4e Change to WEBAPP_URL (#7643) 2023-03-10 14:18:03 +00:00
Hariom Balhara 7edebbcef0 Make sure that a hidden field/question is never required (#7652)
* Update required field value as per hidden field value

* Translate Required and Optional

* Translate Label and Placeholder
2023-03-10 14:11:41 +00:00
Hariom Balhara f22bf721f2 Hotfix/Broken EventType duplication (#7655) 2023-03-10 19:12:00 +05:30
Peer Richelsen ba0e443c52 fixed scroll description size, fixed dark mode toggle switch, fixed date height and positioning, fixed date picker container with negative margin (#7656) 2023-03-10 13:02:48 +00:00
Efraín Rochín aebfc7362e set isBefore or isSame to figure out the months between dates (#7649) 2023-03-10 12:24:10 +00:00
Hariom BalharaandPeer Richelsen 6d82e3f1b6 Move routing forms outside of ee and use LicenseRequired component instead. (#7639)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-09 22:58:02 +00:00
dbf74890a5 Only show smsReminderNumber when workflow has ATTENDEE_NUMBER as action (#7567)
* fix smsReminderNumber booking field

* fix sms reminder booking field when updating steps

* code clean up

* use removeSmsReminderFieldForBooking

* Add smsReminderNumber at correct position

* Fix comments

* code clean up

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-03-10 01:09:42 +05:30
Hariom Balhara 95c1c81b3b Fix ColorPicker not showing up and Auto theme not working (#7626) 2023-03-09 11:57:59 -07:00
Peer Richelsen 2305b926cd fixed timezone select (#7638)
release.yml / release (push) Canceled after 0s
2023-03-09 15:23:56 +00:00
d9a555d94a Additional fields as variables for custom event name (#7454)
* feat: add custom validate name util

* refactor: separate custom event type modal into a
different component

* feat: add validation to zod

* chore: add i18n key

* feat: add dynamic imports

* fix: padding

* Omit cache-hit exit 1, assuming it'll fail regardless

* allow custom inputs as variables in event name

* fix ui for adding custom inputs

* show edited event name in modal

* code clean up

* merge fixes

* includes new booking fields logic

* fix event name with custom booking fields

* code clean up

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

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

* fix type error

* remove old reqbody variable

---------

Co-authored-by: nafees nazik <nafeesnazik21@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-03-09 15:11:16 +00:00
Miguel Nieto 943142e77f Add google-calendar dependency to Cron app (#7636) 2023-03-09 14:57:05 +00:00
Miguel Nieto 4ea729ca53 feat: Cron app (#7446) 2023-03-09 14:13:12 +00:00
Alex van Andel fa383d671b v2.6.9
release.yml / release (push) Canceled after 0s
2023-03-09 13:45:00 +00:00
Peer Richelsen 77bfd3773b fixed back button in settings (#7634) 2023-03-09 13:44:00 +00:00
Peer RichelsenandAlex van Andel 4cd32fca4a new error for no available user (#7609)
* new error for no available user

* Added cursor-pointer & extra whitespace before 'go back.'

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-09 13:11:07 +00:00
c374f4fa20 Fix: about section editor (#7287)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-09 13:06:37 +00:00
Udit TakkarandPeer Richelsen 2ddada49f9 fix: use /avatar url (#7328)
* fix: use /avatar url

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

* fix: don't fetch avatar

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>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-09 12:17:13 +00:00
GitStart-Cal.comandgitstart-calcom feb8218e4e Routing form UI update (#7452)
* Routing form UI update

* Routing form UI update

---------

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-03-09 12:14:17 +00:00
Udit Takkar df4fedd61d fix: remove items-center and add fle x shrink (#7624)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-03-09 11:53:26 +00:00
Udit Takkar d80581ad58 fix: prevent refetching country (#7622)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-03-09 10:38:57 +00:00
Umair FarooqandUmair Farooq 65687d6110 Fixed zapier template UI spacing issue (#7582)
* Fixed zapier template UI spacing issue

* Added spacing

---------

Co-authored-by: Umair Farooq <umair.farooq@morressier.com>
2023-03-09 10:22:30 +00:00
Nafees Nazik b3c973e824 fix: deprecated warning (#7620) 2023-03-09 10:19:00 +00:00
Efraín RochínandPeer Richelsen 913466d06d fix/[cal-1091]-Changing the timezone in profile settings doesn't change the date of the booking - only the time (#7598)
* set timezone to date and time displayed on booking details page

* type fixes

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-09 10:18:22 +00:00
Leo Giovanetti 18c1a6a3a2 Fixing sessionTimeout conversion (#7611) 2023-03-09 10:16:19 +00:00
Nafees Nazik fe17203cea fix: select (#7612) 2023-03-09 10:13:39 +00:00
Leo Giovanetti 3a6f05f1e3 Fixing multiple duration not selected correctly (#7597) 2023-03-09 10:12:37 +00:00
Jeroen Reumkens 5ebbd514d8 Added serverside translations to the forgot password page. (#7618) 2023-03-09 09:14:04 +00:00
58b439ca65 Revamp Google Cal warning for Meet, Amie, and Vimcal (#7308)
* Create requires Google Cal component

* Create installed GCal message

* Move requires GCal component to App

* Clean up

* Abstract prerequisite component

* Add requires message on app card

* Refactor to dependency

* Clean up

* Change typeform dep & remove app card dep component

* Clean up

* Change dependency to dependencies

* Pass disableInstall to default install button for AppCard

* Refactor app page to dependencies

* Type fix

* More type fixes

* Update apps/web/components/apps/App.tsx

* Apply suggestions from code review

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-03-09 10:07:23 +01:00
Udit Takkar c32aadf297 fix: ui inconsistency in calendar app (#7209)
* fix: ui inconsistency in calendar app

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

* fix: remove border

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

* fix: ui bug

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-03-09 00:16:26 +01:00
736c30f134 injects secrets with infisical (#7105)
* injects secrets with infisical

* Update package.json

* Adds infisical project config

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-08 15:28:49 -07:00
github-actions[bot]andCrowdin Bot 7b169ebfc2 New Crowdin translations by Github Action (#7588)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-03-08 23:27:50 +01:00
Alex van Andel 8f5843ab95 Use inviteeTimeZone instead of organizerTimeZone (#7607) 2023-03-08 15:24:11 -07:00
Peer Richelsen 57d17b74c3 Delete issue_label_bot.yaml 2023-03-08 23:04:58 +01:00
Alex van Andelandzomars d40b934866 Bugfix/improved assignment page (#7165)
* Refactor Assignment hosts components

* Addressed NIT

* Remove switch in favour of render object

---------

Co-authored-by: zomars <zomars@me.com>
2023-03-08 15:04:33 -07:00
LucasandPeer Richelsen 2e5c0c6332 Fix/icon size and url (#7605)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-08 21:59:15 +00:00
Peer Richelsen b03bcc7470 Create issue_label_bot.yaml 2023-03-08 22:39:47 +01:00
2da5b19a76 Change quick setup to use docker compose plugin (#7098)
Co-authored-by: Renan <renan.cleyson@moredev.tech>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Omar López <zomars@me.com>
2023-03-08 12:43:39 -07:00
Nafees Nazik 0ee3b26ca8 fix: default conferencing app not showing the correct value after updated (#7589)
* fix: edge case

* feat: add overflow

* fix: invalidate cache
2023-03-08 16:17:08 +00:00
Peer Richelsen e836a042e9 readded overflow-scroll (#7590) 2023-03-08 17:00:59 +01:00
Carina WollendorferandCarinaWolli 37038a0cbf Fixes broken layout of event type description (#7587)
* fix flicker of event type description

* code clean up

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-03-08 15:46:23 +00:00
Omar LópezandAlex van Andel 8b47c064ff Fix conflicts for embed tests (#7311)
* Fix conflicts for embed tests

* Update pr.yml

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-08 13:20:04 +00:00
270 changed files with 3836 additions and 2088 deletions
+1 -1
View File
@@ -30,4 +30,4 @@ packages/features/ee/workflows @CarinaWolli @calcom/core
packages/features/tips @PeerRich @calcom/core
packages/ui @calcom/ui
packages/config @calcom/core
packages/app-store/ee/routing-forms @hariombalhara @calcom/core
packages/app-store/routing-forms @hariombalhara @calcom/core
-3
View File
@@ -77,9 +77,6 @@ lint-results
#Storybook
apps/storybook/build-storybook.log
# Infisical
.infisical.json
# Snaplet
.snaplet/snapshots
.snaplet/structure.d.ts
+8
View File
@@ -0,0 +1,8 @@
{
"workspaceId": "63ea8121b6e2b0543ba79616",
"defaultEnvironment": "dev",
"gitBranchToEnvironmentMapping": {
"main": "staging",
"production": "prod"
}
}
+1 -2
View File
@@ -4,7 +4,7 @@ Contributions are what make the open source community such an amazing place to l
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
## Priorities
## Priorities
<table>
<tr>
@@ -57,7 +57,6 @@ Contributions are what make the open source community such an amazing place to l
</tr>
</table>
## Developing
The development branch is `main`. This is the branch that all pull
+2 -3
View File
@@ -350,17 +350,16 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
</picture>
</a>
<!-- CONTRIBUTORS -->
### Contributors
<a href="https://github.com/calcom/cal.com/graphs/contributors">
<img src="https://contrib.rocks/image?repo=calcom/cal.com" />
</a>
<!-- TRANSLATIONS -->
### Translations
+1 -1
View File
@@ -64,7 +64,7 @@ export default function AppListCard(props: AppListCardProps) {
return (
<div className={`${highlight ? "bg-yellow-100" : ""}`}>
<div className="flex gap-x-3 p-4">
<div className="flex gap-x-3 px-5 py-4">
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
<div className="flex grow flex-col gap-y-1 truncate">
<div className="flex items-center gap-x-2">
+10 -1
View File
@@ -669,8 +669,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
embedUrl: string;
}) => {
const { t } = useLocale();
const router = useRouter();
const iframeRef = useRef<HTMLIFrameElement>(null);
const dialogContentRef = useRef<HTMLDivElement>(null);
const s = (href: string) => {
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
const [a, b] = href.split("=");
@@ -809,7 +812,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
];
return (
<DialogContent size="xl" className="p-0.5" type="creation">
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation">
<div className="flex">
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
@@ -960,6 +963,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div>Button Color</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
@@ -979,6 +984,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div>Text Color</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
@@ -1060,6 +1067,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div className="mb-2 pt-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
+27 -1
View File
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import React, { useState } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton } from "@calcom/app-store/components";
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
import Shell from "@calcom/features/shell/Shell";
@@ -24,6 +24,8 @@ import {
FiShield,
} from "@calcom/ui/components/icon";
/* These app slugs all require Google Cal to be installed */
const Component = ({
name,
type,
@@ -45,6 +47,7 @@ const Component = ({
isProOnly,
images,
isTemplate,
dependencies,
}: Parameters<typeof App>[0]) => {
const { t } = useLocale();
const hasImages = images && images.length > 0;
@@ -76,6 +79,15 @@ const Component = ({
}
);
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
enabled: !!dependencies,
});
const disableInstall =
dependencyData.data && dependencyData.data.some((dependency) => !dependency.installed);
// const disableInstall = requiresGCal && !gCalInstalled.data;
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
// Such apps, can only be installed once.
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
@@ -137,6 +149,7 @@ const Component = ({
<InstallAppButton
type={type}
isProOnly={isProOnly}
disableInstall={disableInstall}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
@@ -176,6 +189,7 @@ const Component = ({
<InstallAppButton
type={type}
isProOnly={isProOnly}
disableInstall={disableInstall}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
@@ -203,6 +217,16 @@ const Component = ({
) : (
<SkeletonButton className="h-10 w-24" />
)}
{dependencies &&
(!dependencyData.isLoading ? (
<div className="mt-6">
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
</div>
) : (
<SkeletonButton className="mt-6 h-20 grow" />
))}
{price !== 0 && (
<span className="block text-right">
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
@@ -332,6 +356,8 @@ export default function App(props: {
isProOnly: AppType["isProOnly"];
images?: string[];
isTemplate?: boolean;
disableInstall?: boolean;
dependencies?: string[];
}) {
return (
<Shell smallHeading isPublic heading={<ShellHeading />} backPath="/apps" withoutSeo>
@@ -88,7 +88,7 @@ function CalendarSwitch(props: {
}
);
return (
<div className="flex py-1">
<div className="flex flex-col py-1 sm:flex-row">
<Switch
key={props.externalId}
name="enabled"
@@ -99,7 +99,7 @@ function CalendarSwitch(props: {
}}
/>
{!!props.destination && (
<span className="ml-4 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800">
<span className="ml-8 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800 sm:ml-4">
<FiArrowLeft className="h-4 w-4" />
{t("adding_events_to")}
</span>
@@ -184,8 +184,8 @@ function ConnectedCalendarsList(props: Props) {
<div className="border-t border-gray-200">
{!fromOnboarding && (
<>
<p className="px-4 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
<ul className="space-y-2 p-4">
<p className="px-5 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
<ul className="space-y-2 px-5 py-4">
{item.calendars.map((cal) => (
<CalendarSwitch
key={cal.externalId}
@@ -264,7 +264,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
<>
{heading && (
<div className="flex flex-col gap-6 rounded-md border border-gray-200 p-7">
<div className="flex flex-col gap-6 rounded-md border p-7">
<ShellSubHeading
title={t("calendar")}
subtitle={t("installed_app_calendar_description")}
@@ -39,7 +39,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? (
<FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
) : (
<img
src={eventLocationType.iconUrl}
@@ -64,13 +64,13 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
return (
<div ref={slotPickerRef}>
{!!date ? (
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
<div className="mb-6 flex items-center text-left text-base">
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
<div className="mb-4 flex items-center text-left text-base">
<div className="mr-4">
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span>
<span className="text-bookinglight font-medium">
<span className="text-bookinglight">
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
</span>
</div>
@@ -85,7 +85,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
/>
</div>
</div>
<div ref={ref} className="flex-grow overflow-y-auto sm:block md:h-[364px]">
<div
ref={ref}
className="scroll-bar scrollbar-track-w-20 relative -mb-4 flex-grow overflow-y-auto sm:block md:h-[364px]">
{slots.length > 0 &&
slots.map((slot) => {
type BookingURL = {
@@ -46,14 +46,16 @@ const BookingDescription: FC<Props> = (props) => {
const { profile, eventType, isBookingPage = false, children } = props;
const { date: bookingDate } = useRouterQuery("date");
const { t } = useLocale();
const { duration = eventType.length.toString(), setQuery: setDuration } = useRouterQuery("duration");
const { duration, setQuery: setDuration } = useRouterQuery("duration");
useEffect(() => {
if (
eventType.metadata?.multipleDuration &&
!eventType.metadata?.multipleDuration?.includes(Number(duration))
!duration ||
isNaN(Number(duration)) ||
(eventType.metadata?.multipleDuration &&
!eventType.metadata?.multipleDuration.includes(Number(duration)))
) {
setDuration(eventType.length.toString());
setDuration(eventType.length);
}
}, [duration, setDuration, eventType.length, eventType.metadata?.multipleDuration]);
@@ -81,20 +83,20 @@ const BookingDescription: FC<Props> = (props) => {
size="sm"
truncateAfter={3}
/>
<h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
<h2 className="mt-1 mb-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
{profile.name}
</h2>
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold text-gray-900">
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold leading-none text-gray-900">
{eventType.title}
</h1>
<div className="dark:text-darkgray-600 flex flex-col space-y-4 text-sm font-medium text-gray-600">
{eventType?.description && (
<div
className={classNames(
"flex",
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ",
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
)}>
<div className="scroll-bar scrollbar-track-w-20 max-h-[200px] max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
<div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
<EventTypeDescriptionSafeHTML eventType={eventType} />
</div>
</div>
+1 -1
View File
@@ -164,7 +164,7 @@ export const SlotPicker = ({
<DatePicker
isLoading={isLoading}
className={classNames(
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 lg:min-w-[455px]",
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]",
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
)}
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
@@ -168,7 +168,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
<div
className={classNames(
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
"min-w-full md:w-[230px] md:min-w-[230px]",
"min-w-full md:w-[280px] md:min-w-[280px]",
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
)}>
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
@@ -4,7 +4,7 @@ import { useSession } from "next-auth/react";
import dynamic from "next/dynamic";
import Head from "next/head";
import { useRouter } from "next/router";
import { useEffect, useMemo, useReducer, useState } from "react";
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
import { useForm, useFormContext } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
@@ -15,6 +15,7 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import { getEventTypeAppData } from "@calcom/app-store/utils";
import type { LocationObject } from "@calcom/core/location";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
@@ -38,6 +39,7 @@ import useTheme from "@calcom/lib/hooks/useTheme";
import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Button, Form, Tooltip } from "@calcom/ui";
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
@@ -213,6 +215,20 @@ const BookingPage = ({
duration = Number(queryDuration);
}
// This is a workaround for forcing the same time format for both server side rendering and client side rendering
// At initial render, we use the default time format which is 12H
const [withDefaultTimeFormat, setWithDefaultTimeFormat] = useState(true);
const parseDateFunc = useCallback(
(date: string | null | Dayjs) => {
return parseDate(date, i18n, withDefaultTimeFormat);
},
[withDefaultTimeFormat]
);
// After intial render on client side, we let parseDateFunc to use the time format from the localStorage
useEffect(() => {
setWithDefaultTimeFormat(false);
}, []);
useEffect(() => {
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
@@ -370,15 +386,26 @@ const BookingPage = ({
// Calculate the booking date(s)
let recurringStrings: string[] = [],
recurringDates: Date[] = [];
const parseRecurringDatesFunc = useCallback(
(date: string | null | Dayjs, recurringEvent: RecurringEvent, recurringCount: number) => {
return parseRecurringDates(
{
startDate: date,
timeZone: timeZone(),
recurringEvent: recurringEvent,
recurringCount: recurringCount,
withDefaultTimeFormat: withDefaultTimeFormat,
},
i18n
);
},
[withDefaultTimeFormat, date, eventType.recurringEvent, recurringEventCount]
);
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
[recurringStrings, recurringDates] = parseRecurringDates(
{
startDate: date,
timeZone: timeZone(),
recurringEvent: eventType.recurringEvent,
recurringCount: parseInt(recurringEventCount.toString()),
},
i18n
[recurringStrings, recurringDates] = parseRecurringDatesFunc(
date,
eventType.recurringEvent,
parseInt(recurringEventCount.toString())
);
}
@@ -507,7 +534,7 @@ const BookingPage = ({
<div className="text-sm font-medium">
{isClientTimezoneAvailable &&
(rescheduleUid || !eventType.recurringEvent?.freq) &&
`${parseDate(date, i18n)}`}
`${parseDateFunc(date)}`}
{isClientTimezoneAvailable &&
!rescheduleUid &&
eventType.recurringEvent?.freq &&
@@ -537,7 +564,7 @@ const BookingPage = ({
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{isClientTimezoneAvailable &&
typeof booking.startTime === "string" &&
parseDate(dayjs(booking.startTime), i18n)}
parseDateFunc(dayjs(booking.startTime))}
</p>
</div>
)}
@@ -615,17 +642,28 @@ export default BookingPage;
function ErrorMessage({ error }: { error: unknown }) {
const { t } = useLocale();
const { query: { rescheduleUid } = {} } = useRouter();
const router = useRouter();
return (
<div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
<FiAlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
</div>
<div className="ltr:ml-3 rtl:mr-3">
<p className="text-sm text-yellow-700">
<p className="text-sm text-blue-700">
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
{error instanceof HttpError || error instanceof Error ? t(error.message) : "Unknown error"}
{error instanceof HttpError || error instanceof Error ? (
<>
{t("can_you_try_again")}{" "}
<span className="cursor-pointer underline" onClick={() => router.back()}>
{t("go_back")}
</span>
.
</> /* t(error.message) */
) : (
"Unknown error"
)}
</p>
</div>
</div>
@@ -52,7 +52,8 @@ const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
type="text"
placeholder={placeHolder_}
{...register("customEventName", {
validate: (value) => validateCustomEventName(value, t("invalid_event_name_variables")),
validate: (value) =>
validateCustomEventName(value, t("invalid_event_name_variables"), event.bookingFields),
})}
className="mb-0"
/>
@@ -12,6 +12,7 @@ import DestinationCalendarSelector from "@calcom/features/calendars/DestinationC
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
@@ -36,11 +37,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const bookingFields: Prisma.JsonObject = {};
eventType.bookingFields.forEach(({ name }) => {
bookingFields[name] = name + " input";
});
const eventNameObject: EventNameObjectType = {
attendeeName: t("scheduler"),
eventType: eventType.title,
eventName: eventType.eventName,
host: eventType.users[0]?.name || "Nameless",
bookingFields: bookingFields,
t,
};
@@ -308,7 +317,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<CustomEventTypeModal
close={closeEventNameTip}
setValue={setEventName}
defaultValue={eventType.eventName || ""}
defaultValue={formMethods.getValues("eventName") || eventType.eventName || ""}
placeHolder={eventNamePlaceholder}
event={eventNameObject}
/>
+175 -103
View File
@@ -1,9 +1,11 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import type { Key } from "react";
import React, { useEffect, useState } from "react";
import type { UseFormRegisterReturn } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { SingleValue } from "react-select";
import { classNames } from "@calcom/lib";
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
@@ -11,8 +13,17 @@ import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
import findDurationType from "@calcom/lib/findDurationType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { PeriodType } from "@calcom/prisma/client";
import type { BookingLimit } from "@calcom/types/Calendar";
import { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
import type { IntervalLimit } from "@calcom/types/Calendar";
import {
Button,
DateRangePicker,
Input,
InputField,
Label,
Select,
SettingsToggle,
TextField,
} from "@calcom/ui";
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
const MinimumBookingNoticeInput = React.forwardRef<
@@ -260,7 +271,34 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
formMethods.setValue("bookingLimits", {});
}
}}>
<BookingLimits />
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
</SettingsToggle>
)}
/>
<hr />
<Controller
name="durationLimits"
control={formMethods.control}
render={({ field: { value } }) => (
<SettingsToggle
title={t("limit_total_booking_duration")}
description={t("limit_total_booking_duration_description")}
checked={Object.keys(value ?? {}).length > 0}
onCheckedChange={(active) => {
if (active) {
formMethods.setValue("durationLimits", {
PER_DAY: 60,
});
} else {
formMethods.setValue("durationLimits", {});
}
}}>
<IntervalLimitsManager
propertyName="durationLimits"
defaultLimit={60}
step={15}
textFieldSuffix={t("minutes")}
/>
</SettingsToggle>
)}
/>
@@ -348,124 +386,158 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
);
};
const validationOrderKeys = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"];
type BookingLimitsKey = keyof BookingLimit;
const BookingLimits = () => {
type IntervalLimitsKey = keyof IntervalLimit;
const intervalOrderKeys = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"] as const;
const INTERVAL_LIMIT_OPTIONS = intervalOrderKeys.map((key) => ({
value: key as keyof IntervalLimit,
label: `Per ${key.split("_")[1].toLocaleLowerCase()}`,
}));
type IntervalLimitItemProps = {
key: Key;
limitKey: IntervalLimitsKey;
step: number;
value: number;
textFieldSuffix?: string;
selectOptions: { value: keyof IntervalLimit; label: string }[];
hasDeleteButton?: boolean;
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
onLimitChange: (intervalLimitsKey: IntervalLimitsKey, limit: number) => void;
onIntervalSelect: (interval: SingleValue<{ value: keyof IntervalLimit; label: string }>) => void;
};
const IntervalLimitItem = ({
limitKey,
step,
value,
textFieldSuffix,
selectOptions,
hasDeleteButton,
onDelete,
onLimitChange,
onIntervalSelect,
}: IntervalLimitItemProps) => {
return (
<div className="mb-2 flex items-center space-x-2 text-sm rtl:space-x-reverse" key={limitKey}>
<TextField
required
type="number"
containerClassName={`${textFieldSuffix ? "w-36" : "w-16"} -mb-1`}
placeholder={`${value}`}
min={step}
step={step}
defaultValue={value}
addOnSuffix={textFieldSuffix}
onChange={(e) => onLimitChange(limitKey, parseInt(e.target.value))}
/>
<Select
options={selectOptions}
isSearchable={false}
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
onChange={onIntervalSelect}
/>
{hasDeleteButton && (
<Button variant="icon" StartIcon={FiTrash} color="destructive" onClick={() => onDelete(limitKey)} />
)}
</div>
);
};
type IntervalLimitsManagerProps<K extends "durationLimits" | "bookingLimits"> = {
propertyName: K;
defaultLimit: number;
step: number;
textFieldSuffix?: string;
};
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
propertyName,
defaultLimit,
step,
textFieldSuffix,
}: IntervalLimitsManagerProps<K>) => {
const { watch, setValue, control } = useFormContext<FormValues>();
const watchBookingLimits = watch("bookingLimits");
const watchIntervalLimits = watch(propertyName);
const { t } = useLocale();
const [animateRef] = useAutoAnimate<HTMLUListElement>();
const BOOKING_LIMIT_OPTIONS: {
value: keyof BookingLimit;
label: string;
}[] = [
{
value: "PER_DAY",
label: "Per Day",
},
{
value: "PER_WEEK",
label: "Per Week",
},
{
value: "PER_MONTH",
label: "Per Month",
},
{
value: "PER_YEAR",
label: "Per Year",
},
];
return (
<Controller
name="bookingLimits"
name={propertyName}
control={control}
render={({ field: { value, onChange } }) => {
const currentBookingLimits = value;
const currentIntervalLimits = value;
const addLimit = () => {
if (!currentIntervalLimits || !watchIntervalLimits) return;
const currentKeys = Object.keys(watchIntervalLimits);
const [rest] = Object.values(INTERVAL_LIMIT_OPTIONS).filter(
(option) => !currentKeys.includes(option.value)
);
if (!rest || !currentKeys.length) return;
//currentDurationLimits is always defined so can be casted
// @ts-expect-error FIXME Fix these typings
setValue(propertyName, {
...watchIntervalLimits,
[rest.value]: defaultLimit,
});
};
return (
<ul ref={animateRef}>
{currentBookingLimits &&
watchBookingLimits &&
Object.entries(currentBookingLimits)
.sort(([limitkeyA], [limitKeyB]) => {
{currentIntervalLimits &&
watchIntervalLimits &&
Object.entries(currentIntervalLimits)
.sort(([limitKeyA], [limitKeyB]) => {
return (
validationOrderKeys.indexOf(limitkeyA as BookingLimitsKey) -
validationOrderKeys.indexOf(limitKeyB as BookingLimitsKey)
intervalOrderKeys.indexOf(limitKeyA as IntervalLimitsKey) -
intervalOrderKeys.indexOf(limitKeyB as IntervalLimitsKey)
);
})
.map(([key, bookingAmount]) => {
const bookingLimitKey = key as BookingLimitsKey;
.map(([key, value]) => {
const limitKey = key as IntervalLimitsKey;
return (
<div
className="mb-2 flex items-center space-x-2 text-sm rtl:space-x-reverse"
key={bookingLimitKey}>
<Input
id={`${bookingLimitKey}-limit`}
type="number"
className="mb-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
placeholder="1"
min={1}
defaultValue={bookingAmount}
onChange={(e) => {
const val = e.target.value;
setValue(`bookingLimits.${bookingLimitKey}`, parseInt(val));
}}
/>
<Select
options={BOOKING_LIMIT_OPTIONS.filter(
(option) => !Object.keys(currentBookingLimits).includes(option.value)
)}
isSearchable={false}
defaultValue={BOOKING_LIMIT_OPTIONS.find((option) => option.value === key)}
onChange={(val) => {
const current = currentBookingLimits;
const currentValue = watchBookingLimits[bookingLimitKey];
<IntervalLimitItem
key={key}
limitKey={limitKey}
step={step}
value={value}
textFieldSuffix={textFieldSuffix}
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
(option) => !Object.keys(currentIntervalLimits).includes(option.value)
)}
onLimitChange={(intervalLimitKey, val) =>
// @ts-expect-error FIXME Fix these typings
setValue(`${propertyName}.${intervalLimitKey}`, val)
}
onDelete={(intervalLimitKey) => {
const current = currentIntervalLimits;
delete current[intervalLimitKey];
onChange(current);
}}
onIntervalSelect={(interval) => {
const current = currentIntervalLimits;
const currentValue = watchIntervalLimits[limitKey];
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
delete current[bookingLimitKey];
const newData = {
...current,
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
[val?.value as BookingLimitsKey]: currentValue,
};
onChange(newData);
}}
/>
<Button
variant="icon"
StartIcon={FiTrash}
color="destructive"
onClick={() => {
const current = currentBookingLimits;
delete current[key as BookingLimitsKey];
onChange(current);
}}
/>
</div>
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
delete current[limitKey];
const newData = {
...current,
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
[interval?.value as IntervalLimitsKey]: currentValue,
};
onChange(newData);
}}
/>
);
})}
{currentBookingLimits && Object.keys(currentBookingLimits).length <= 3 && (
<Button
color="minimal"
StartIcon={FiPlus}
onClick={() => {
if (!currentBookingLimits || !watchBookingLimits) return;
const currentKeys = Object.keys(watchBookingLimits);
const rest = Object.values(BOOKING_LIMIT_OPTIONS).filter(
(option) => !currentKeys.includes(option.value)
);
if (!rest || !currentKeys) return;
//currentBookingLimits is always defined so can be casted
setValue("bookingLimits", {
...watchBookingLimits,
[rest[0].value]: 1,
});
}}>
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && (
<Button color="minimal" StartIcon={FiPlus} onClick={addLimit}>
{t("add_limit")}
</Button>
)}
+145 -111
View File
@@ -1,7 +1,7 @@
import { SchedulingType } from "@prisma/client";
import type { SchedulingType } from "@prisma/client";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef } from "react";
import type { ComponentProps } from "react";
import type { Control } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { Options } from "react-select";
@@ -35,65 +35,169 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
return 0;
};
const FixedHosts = ({
control,
const CheckedHostField = ({
labelText,
placeholder,
options = [],
isFixed,
value,
onChange,
...rest
}: {
control: Control<FormValues>;
labelText: string;
placeholder: string;
isFixed: boolean;
value: { isFixed: boolean; userId: number }[];
onChange?: (options: { isFixed: boolean; userId: number }[]) => void;
options?: Options<CheckedSelectOption>;
} & Partial<ComponentProps<typeof CheckedTeamSelect>>) => {
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
return (
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
<div>
<Label>{labelText}</Label>
<Controller
name="hostsFixed"
control={control}
render={({ field: { onChange, value } }) => {
return (
<CheckedTeamSelect
isDisabled={false}
onChange={(options) => {
onChange(
options.map((option) => ({
isFixed: true,
userId: parseInt(option.value, 10),
}))
);
}}
value={value
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={options}
placeholder={placeholder}
{...rest}
/>
);
<CheckedTeamSelect
isOptionDisabled={(option) => !!value.find((host) => host.userId.toString() === option.value)}
onChange={(options) => {
onChange &&
onChange(
options.map((option) => ({
isFixed,
userId: parseInt(option.value, 10),
}))
);
}}
value={(value || [])
.filter(({ isFixed: _isFixed }) => isFixed === _isFixed)
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={options}
placeholder={placeholder}
{...rest}
/>
</div>
</div>
);
};
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
const formMethods = useFormContext<FormValues>();
const RoundRobinHosts = ({
teamMembers,
value,
onChange,
}: {
value: { isFixed: boolean; userId: number }[];
onChange: (hosts: { isFixed: boolean; userId: number }[]) => void;
teamMembers: {
value: string;
label: string;
avatar: string;
email: string;
}[];
}) => {
const { t } = useLocale();
return (
<>
<CheckedHostField
options={teamMembers.sort(sortByLabel)}
isFixed={true}
onChange={(changeValue) => {
onChange([...value.filter(({ isFixed }) => !isFixed), ...changeValue]);
}}
value={value}
placeholder={t("add_fixed_hosts")}
labelText={t("fixed_hosts")}
/>
<CheckedHostField
options={teamMembers.sort(sortByLabel)}
onChange={(changeValue) => onChange([...value.filter(({ isFixed }) => isFixed), ...changeValue])}
value={value}
isFixed={false}
placeholder={t("add_attendees")}
labelText={t("round_robin_hosts")}
/>
</>
);
};
const Hosts = ({
teamMembers,
}: {
teamMembers: {
value: string;
label: string;
avatar: string;
email: string;
}[];
}) => {
const { t } = useLocale();
const {
control,
resetField,
getValues,
formState: { submitCount },
} = useFormContext<FormValues>();
const schedulingType = useWatch({
control: formMethods.control,
control,
name: "schedulingType",
});
const initialValue = useRef<{
hosts: FormValues["hosts"];
schedulingType: SchedulingType | null;
submitCount: number;
} | null>(null);
useEffect(() => {
// Handles init & out of date initial value after submission.
if (!initialValue.current || initialValue.current?.submitCount !== submitCount) {
initialValue.current = { hosts: getValues("hosts"), schedulingType, submitCount };
return;
}
resetField("hosts", {
defaultValue: initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
});
}, [schedulingType, resetField, getValues, submitCount]);
return (
<Controller<FormValues>
name="hosts"
render={({ field: { onChange, value } }) => {
const schedulingTypeRender = {
COLLECTIVE: (
<CheckedHostField
value={value}
onChange={onChange}
isFixed={true}
options={teamMembers.sort(sortByLabel)}
placeholder={t("add_attendees")}
labelText={t("team")}
/>
),
ROUND_ROBIN: (
<>
<RoundRobinHosts teamMembers={teamMembers} onChange={onChange} value={value} />
{/*<TextField
required
type="number"
label={t("minimum_round_robin_hosts_count")}
defaultValue={1}
{...formMethods.register("minimumHostCount")}
addOnSuffix={<>{t("hosts")}</>}
/>*/}
</>
),
};
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
}}
/>
);
};
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
const { t } = useLocale();
const schedulingTypeOptions: {
value: SchedulingType;
@@ -101,17 +205,16 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
// description: string;
}[] = [
{
value: SchedulingType.COLLECTIVE,
value: "COLLECTIVE",
label: t("collective"),
// description: t("collective_description"),
},
{
value: SchedulingType.ROUND_ROBIN,
value: "ROUND_ROBIN",
label: t("round_robin"),
// description: t("round_robin_description"),
},
];
const teamMembersOptions = teamMembers.map(mapUserToValue);
return (
<div>
@@ -119,9 +222,8 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
<div className="space-y-5">
<div className="flex flex-col">
<Label>{t("scheduling_type")}</Label>
<Controller
<Controller<FormValues>
name="schedulingType"
control={formMethods.control}
render={({ field: { value, onChange } }) => (
<Select
options={schedulingTypeOptions}
@@ -134,75 +236,7 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
)}
/>
</div>
{schedulingType === SchedulingType.COLLECTIVE && (
<FixedHosts
options={teamMembersOptions.sort(sortByLabel)}
placeholder={t("add_attendees")}
labelText={t("team")}
control={formMethods.control}
/>
)}
{schedulingType === SchedulingType.ROUND_ROBIN && (
<>
<FixedHosts
options={teamMembersOptions.sort(sortByLabel)}
isOptionDisabled={(option) =>
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
}
placeholder={t("add_fixed_hosts")}
labelText={t("fixed_hosts")}
control={formMethods.control}
/>
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
<div>
<Label>{t("round_robin_hosts")}</Label>
<Controller
name="hosts"
control={formMethods.control}
render={({ field: { onChange, value } }) => (
<CheckedTeamSelect
isDisabled={false}
onChange={(options) =>
onChange(
options.map((option) => ({
isFixed: false,
userId: parseInt(option.value, 10),
}))
)
}
value={value
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
teamMembers
.map(mapUserToValue)
.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={teamMembersOptions.sort(sortByLabel)}
isOptionDisabled={(option) =>
!!formMethods
.getValues("hostsFixed")
.find((host) => host.userId.toString() === option.value)
}
placeholder={t("add_attendees")}
/>
)}
/>
</div>
{/*<TextField
required
type="number"
label={t("minimum_round_robin_hosts_count")}
defaultValue={1}
{...formMethods.register("minimumHostCount")}
addOnSuffix={<>{t("hosts")}</>}
/>*/}
</div>
</>
)}
<Hosts teamMembers={teamMembersOptions} />
</div>
)}
</div>
@@ -1,7 +1,7 @@
import type { SyntheticEvent } from "react";
import { useState } from "react";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast } from "@calcom/ui";
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
@@ -2,7 +2,7 @@ import type { BaseSyntheticEvent } from "react";
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form } from "@calcom/ui";
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
@@ -2,7 +2,7 @@ import type { BaseSyntheticEvent } from "react";
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form, TextField } from "@calcom/ui";
+1 -1
View File
@@ -5,7 +5,7 @@ import React from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import * as z from "zod";
import { isPasswordValid } from "@calcom/lib/auth";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
-45
View File
@@ -1,45 +0,0 @@
import { IdentityProvider } from "@prisma/client";
import { compare, hash } from "bcryptjs";
import type { Session } from "next-auth";
import type { GetSessionParams } from "next-auth/react";
import { getSession as getSessionInner } from "next-auth/react";
/** @deprecated use the one from `@calcom/lib/auth` */
export async function hashPassword(password: string) {
const hashedPassword = await hash(password, 12);
return hashedPassword;
}
/** @deprecated use the one from `@calcom/lib/auth` */
export async function verifyPassword(password: string, hashedPassword: string) {
const isValid = await compare(password, hashedPassword);
return isValid;
}
/** @deprecated use the one from `@calcom/lib/auth` */
export async function getSession(options: GetSessionParams): Promise<Session | null> {
const session = await getSessionInner(options);
// that these are equal are ensured in `[...nextauth]`'s callback
return session as Session | null;
}
/** @deprecated use the one from `@calcom/lib/auth` */
export enum ErrorCode {
UserNotFound = "user-not-found",
IncorrectPassword = "incorrect-password",
UserMissingPassword = "missing-password",
TwoFactorDisabled = "two-factor-disabled",
TwoFactorAlreadyEnabled = "two-factor-already-enabled",
TwoFactorSetupRequired = "two-factor-setup-required",
SecondFactorRequired = "second-factor-required",
IncorrectTwoFactorCode = "incorrect-two-factor-code",
InternalServerError = "internal-server-error",
NewPasswordMatchesOld = "new-password-matches-old",
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
RateLimitExceeded = "rate-limit-exceeded",
InvalidPassword = "invalid-password",
}
/** @deprecated use the one from `@calcom/lib/auth` */
export const identityProviderNameMap: { [key in IdentityProvider]: string } = {
[IdentityProvider.CAL]: "Cal",
[IdentityProvider.GOOGLE]: "Google",
[IdentityProvider.SAML]: "SAML",
};
+9 -5
View File
@@ -1,10 +1,10 @@
import parser from "accept-language-parser";
import type { IncomingMessage } from "http";
import type { GetServerSidePropsContext } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { Maybe } from "@calcom/trpc/server";
import { getSession } from "@lib/auth";
import { i18n } from "../../../next-i18next.config";
export function getLocaleFromHeaders(req: IncomingMessage): string {
@@ -15,9 +15,13 @@ export function getLocaleFromHeaders(req: IncomingMessage): string {
return preferredLocale ?? i18n.defaultLocale;
}
export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage): Promise<string> => {
const prisma = (await import("@calcom/prisma")).default;
const session = await getSession({ req });
export const getOrSetUserLocaleFromHeaders = async (
req: GetServerSidePropsContext["req"],
res: GetServerSidePropsContext["res"]
): Promise<string> => {
const { default: prisma } = await import("@calcom/prisma");
const session = await getServerSession({ req, res });
const preferredLocale = getLocaleFromHeaders(req);
if (session?.user?.id) {
+10 -6
View File
@@ -3,21 +3,23 @@ import { RRule } from "rrule";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
import { detectBrowserTimeFormat, TimeFormat } from "@calcom/lib/timeFormat";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { parseZone } from "./parseZone";
const processDate = (date: string | null | Dayjs, i18n: I18n) => {
const processDate = (date: string | null | Dayjs, i18n: I18n, withDefaultTimeFormat: boolean) => {
const parsedZone = parseZone(date);
if (!parsedZone?.isValid()) return "Invalid date";
const formattedTime = parsedZone?.format(detectBrowserTimeFormat);
const formattedTime = parsedZone?.format(
withDefaultTimeFormat ? TimeFormat.TWELVE_HOUR : detectBrowserTimeFormat
);
return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" });
};
export const parseDate = (date: string | null | Dayjs, i18n: I18n) => {
export const parseDate = (date: string | null | Dayjs, i18n: I18n, withDefaultTimeFormat: boolean) => {
if (!date) return ["No date"];
return processDate(date, i18n);
return processDate(date, i18n, withDefaultTimeFormat);
};
export const parseRecurringDates = (
@@ -26,11 +28,13 @@ export const parseRecurringDates = (
timeZone,
recurringEvent,
recurringCount,
withDefaultTimeFormat,
}: {
startDate: string | null | Dayjs;
timeZone?: string;
recurringEvent: RecurringEvent | null;
recurringCount: number;
withDefaultTimeFormat: boolean;
},
i18n: I18n
): [string[], Date[]] => {
@@ -50,7 +54,7 @@ export const parseRecurringDates = (
});
const dateStrings = times.map((t) => {
// finally; show in local timeZone again
return processDate(t.tz(timeZone), i18n);
return processDate(t.tz(timeZone), i18n, withDefaultTimeFormat);
});
return [dateStrings, times.map((t) => t.toDate())];
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.6.8",
"version": "2.6.12",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -86,7 +86,7 @@
"micro": "^10.0.1",
"mime-types": "^2.1.35",
"next": "^13.2.1",
"next-auth": "^4.18.8",
"next-auth": "^4.20.1",
"next-axiom": "^0.16.0",
"next-collect": "^0.2.1",
"next-i18next": "^11.3.0",
+30 -23
View File
@@ -15,6 +15,7 @@ import {
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import CustomBranding from "@calcom/lib/CustomBranding";
import { WEBAPP_URL } from "@calcom/lib/constants";
import defaultEvents, {
getDynamicEventDescription,
getGroupName,
@@ -76,7 +77,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
size="sm"
items={props.users.map((user) => ({
alt: user.name || "",
image: user.avatar || "",
image: user.avatar,
}))}
/>
</div>
@@ -177,26 +178,28 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
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
prefetch={false}
href={{
pathname: `/${user.username}/${type.slug}`,
query,
}}
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
className="block w-full p-5"
data-testid="event-type-link">
<div className="flex flex-wrap items-center">
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
{type.title}
</h2>
</div>
<EventTypeDescription eventType={type} />
</Link>
<div className="block w-full p-5">
<Link
prefetch={false}
href={{
pathname: `/${user.username}/${type.slug}`,
query,
}}
passHref
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
data-testid="event-type-link">
<div className="flex flex-wrap items-center">
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
{type.title}
</h2>
</div>
<EventTypeDescription eventType={type} />
</Link>
</div>
</div>
))
)}
@@ -259,7 +262,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const usernameList = getUsernameList(context.query.user as string);
const dataFetchStart = Date.now();
const users = await prisma.user.findMany({
const usersWithoutAvatar = await prisma.user.findMany({
where: {
username: {
in: usernameList,
@@ -273,7 +276,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
bio: true,
brandColor: true,
darkBrandColor: true,
avatar: true,
theme: true,
away: true,
verified: true,
@@ -281,6 +283,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
});
const users = usersWithoutAvatar.map((user) => ({
...user,
avatar: `${WEBAPP_URL}/${user.username}/avatar.png`,
}));
if (!users.length) {
return {
notFound: true,
+2 -656
View File
@@ -1,659 +1,5 @@
import type { UserPermissionRole } from "@prisma/client";
import { IdentityProvider } from "@prisma/client";
import { readFileSync } from "fs";
import Handlebars from "handlebars";
import { SignJWT } from "jose";
import type { Session } from "next-auth";
import NextAuth from "next-auth";
import { encode } from "next-auth/jwt";
import type { Provider } from "next-auth/providers";
import CredentialsProvider from "next-auth/providers/credentials";
import EmailProvider from "next-auth/providers/email";
import GoogleProvider from "next-auth/providers/google";
import type { TransportOptions } from "nodemailer";
import nodemailer from "nodemailer";
import { authenticator } from "otplib";
import path from "path";
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { hostedCal, isSAMLLoginEnabled, clientSecretVerifier } from "@calcom/features/ee/sso/lib/saml";
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies";
import { randomString } from "@calcom/lib/random";
import rateLimit from "@calcom/lib/rateLimit";
import { serverConfig } from "@calcom/lib/serverConfig";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options";
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
const transporter = nodemailer.createTransport<TransportOptions>({
...(serverConfig.transport as TransportOptions),
} as TransportOptions);
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
const signJwt = async (payload: { email: string }) => {
const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY);
return new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.setSubject(payload.email)
.setIssuedAt()
.setIssuer(WEBSITE_URL)
.setAudience(`${WEBSITE_URL}/auth/login`)
.setExpirationTime("2m")
.sign(secret);
};
const loginWithTotp = async (user: { email: string }) =>
`/auth/login?totp=${await signJwt({ email: user.email })}`;
const providers: Provider[] = [
CredentialsProvider({
id: "credentials",
name: "Cal.com",
type: "credentials",
credentials: {
email: { label: "Email Address", type: "email", placeholder: "john.doe@example.com" },
password: { label: "Password", type: "password", placeholder: "Your super secure password" },
totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" },
},
async authorize(credentials) {
if (!credentials) {
console.error(`For some reason credentials are missing`);
throw new Error(ErrorCode.InternalServerError);
}
const user = await prisma.user.findUnique({
where: {
email: credentials.email.toLowerCase(),
},
select: {
role: true,
id: true,
username: true,
name: true,
email: true,
metadata: true,
identityProvider: true,
password: true,
twoFactorEnabled: true,
twoFactorSecret: true,
teams: {
include: {
team: true,
},
},
},
});
// Don't leak information about it being username or password that is invalid
if (!user) {
throw new Error(ErrorCode.IncorrectUsernamePassword);
}
if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
}
if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
throw new Error(ErrorCode.IncorrectUsernamePassword);
}
if (user.password) {
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
if (!isCorrectPassword) {
throw new Error(ErrorCode.IncorrectUsernamePassword);
}
}
if (user.twoFactorEnabled) {
if (!credentials.totpCode) {
throw new Error(ErrorCode.SecondFactorRequired);
}
if (!user.twoFactorSecret) {
console.error(`Two factor is enabled for user ${user.id} but they have no secret`);
throw new Error(ErrorCode.InternalServerError);
}
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error(`"Missing encryption key; cannot proceed with two factor login."`);
throw new Error(ErrorCode.InternalServerError);
}
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
if (secret.length !== 32) {
console.error(
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
);
throw new Error(ErrorCode.InternalServerError);
}
const isValidToken = authenticator.check(credentials.totpCode, secret);
if (!isValidToken) {
throw new Error(ErrorCode.IncorrectTwoFactorCode);
}
}
const limiter = rateLimit({
intervalInMs: 60 * 1000, // 1 minute
});
await limiter.check(10, user.email); // 10 requests per minute
// Check if the user you are logging into has any active teams
const hasActiveTeams =
user.teams.filter((m: { team: { metadata: unknown } }) => {
if (!IS_TEAM_BILLING_ENABLED) return true;
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return true;
return false;
}).length > 0;
// authentication success- but does it meet the minimum password requirements?
if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) {
return {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
role: "INACTIVE_ADMIN",
belongsToActiveTeam: hasActiveTeams,
};
}
return {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
role: user.role,
belongsToActiveTeam: hasActiveTeams,
};
},
}),
ImpersonationProvider,
];
if (IS_GOOGLE_LOGIN_ENABLED) {
providers.push(
GoogleProvider({
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
allowDangerousEmailAccountLinking: true,
})
);
}
if (isSAMLLoginEnabled) {
providers.push({
id: "saml",
name: "BoxyHQ",
type: "oauth",
version: "2.0",
checks: ["pkce", "state"],
authorization: {
url: `${WEBAPP_URL}/api/auth/saml/authorize`,
params: {
scope: "",
response_type: "code",
provider: "saml",
},
},
token: {
url: `${WEBAPP_URL}/api/auth/saml/token`,
params: { grant_type: "authorization_code" },
},
userinfo: `${WEBAPP_URL}/api/auth/saml/userinfo`,
profile: (profile) => {
return {
id: profile.id || "",
firstName: profile.firstName || "",
lastName: profile.lastName || "",
email: profile.email || "",
name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
email_verified: true,
};
},
options: {
clientId: "dummy",
clientSecret: clientSecretVerifier,
},
allowDangerousEmailAccountLinking: true,
});
// Idp initiated login
providers.push(
CredentialsProvider({
id: "saml-idp",
name: "IdP Login",
credentials: {
code: {},
},
async authorize(credentials) {
if (!credentials) {
return null;
}
const { code } = credentials;
if (!code) {
return null;
}
const { oauthController } = await jackson();
// Fetch access token
const { access_token } = await oauthController.token({
code,
grant_type: "authorization_code",
redirect_uri: `${process.env.NEXTAUTH_URL}`,
client_id: "dummy",
client_secret: clientSecretVerifier,
});
if (!access_token) {
return null;
}
// Fetch user info
const userInfo = await oauthController.userInfo(access_token);
if (!userInfo) {
return null;
}
const { id, firstName, lastName, email } = userInfo;
return {
id: id as unknown as number,
firstName,
lastName,
email,
name: `${firstName} ${lastName}`.trim(),
email_verified: true,
};
},
})
);
}
if (true) {
const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates");
providers.push(
EmailProvider({
type: "email",
maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only
// Here we setup the sendVerificationRequest that calls the email template with the identifier (email) and token to verify.
sendVerificationRequest: ({ identifier, url }) => {
const originalUrl = new URL(url);
const webappUrl = new URL(WEBAPP_URL);
if (originalUrl.origin !== webappUrl.origin) {
url = url.replace(originalUrl.origin, webappUrl.origin);
}
const emailFile = readFileSync(path.join(emailsDir, "confirm-email.html"), {
encoding: "utf8",
});
const emailTemplate = Handlebars.compile(emailFile);
transporter.sendMail({
from: `${process.env.EMAIL_FROM}` || APP_NAME,
to: identifier,
subject: "Your sign-in link for " + APP_NAME,
html: emailTemplate({
base_url: WEBAPP_URL,
signin_url: url,
email: identifier,
}),
});
},
})
);
}
function isNumber(n: string) {
return !isNaN(parseFloat(n)) && !isNaN(+n);
}
const calcomAdapter = CalComAdapter(prisma);
export default NextAuth({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
adapter: calcomAdapter,
session: {
strategy: "jwt",
},
jwt: {
// decorate the native JWT encode function
// Impl. detail: We don't pass through as this function is called with encode/decode functions.
encode: async ({ token, maxAge, secret }) => {
if (token?.sub && isNumber(token.sub)) {
const user = await prisma.user.findFirst({
where: { id: Number(token.sub) },
select: { metadata: true },
});
// if no user is found, we still don't want to crash here.
if (user) {
const metadata = userMetadata.parse(user.metadata);
if (metadata?.sessionTimeout) {
maxAge = metadata.sessionTimeout / 60;
}
}
}
return encode({ secret, token, maxAge });
},
},
cookies: defaultCookies(WEBAPP_URL?.startsWith("https://")),
pages: {
signIn: "/auth/login",
signOut: "/auth/logout",
error: "/auth/error", // Error code passed in query string as ?error=
verifyRequest: "/auth/verify",
// newUser: "/auth/new", // New users will be directed here on first sign in (leave the property out if not of interest)
},
providers,
callbacks: {
async jwt({ token, user, account }) {
const autoMergeIdentities = async () => {
const existingUser = await prisma.user.findFirst({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
where: { email: token.email! },
select: {
id: true,
username: true,
name: true,
email: true,
role: true,
},
});
if (!existingUser) {
return token;
}
return {
...existingUser,
...token,
};
};
if (!user) {
return await autoMergeIdentities();
}
if (!account) {
return token;
}
if (account.type === "credentials") {
// return token if credentials,saml-idp
if (account.provider === "saml-idp") {
return token;
}
// any other credentials, add user info
return {
...token,
id: user.id,
name: user.name,
username: user.username,
email: user.email,
role: user.role,
impersonatedByUID: user?.impersonatedByUID,
belongsToActiveTeam: user?.belongsToActiveTeam,
};
}
// The arguments above are from the provider so we need to look up the
// user based on those values in order to construct a JWT.
if (account.type === "oauth") {
if (!account.provider || !account.providerAccountId) {
return token;
}
const idP = account.provider === "saml" ? IdentityProvider.SAML : IdentityProvider.GOOGLE;
const existingUser = await prisma.user.findFirst({
where: {
AND: [
{
identityProvider: idP,
},
{
identityProviderId: account.providerAccountId as string,
},
],
},
});
if (!existingUser) {
return await autoMergeIdentities();
}
return {
...token,
id: existingUser.id,
name: existingUser.name,
username: existingUser.username,
email: existingUser.email,
role: existingUser.role,
impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
};
}
return token;
},
async session({ session, token }) {
const hasValidLicense = await checkLicense(prisma);
const calendsoSession: Session = {
...session,
hasValidLicense,
user: {
...session.user,
id: token.id as number,
name: token.name,
username: token.username as string,
role: token.role as UserPermissionRole,
impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
},
};
return calendsoSession;
},
async signIn(params) {
const { user, account, profile } = params;
if (account?.provider === "email") {
return true;
}
// In this case we've already verified the credentials in the authorize
// callback so we can sign the user in.
// Only if provider is not saml-idp
if (account?.provider !== "saml-idp") {
if (account?.type === "credentials") {
return true;
}
if (account?.type !== "oauth") {
return false;
}
}
if (!user.email) {
return false;
}
if (!user.name) {
return false;
}
if (account?.provider) {
let idP: IdentityProvider = IdentityProvider.GOOGLE;
if (account.provider === "saml" || account.provider === "saml-idp") {
idP = IdentityProvider.SAML;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error TODO validate email_verified key on profile
user.email_verified = user.email_verified || !!user.emailVerified || profile.email_verified;
if (!user.email_verified) {
return "/auth/error?error=unverified-email";
}
// Only google oauth on this path
const existingUser = await prisma.user.findFirst({
include: {
accounts: {
where: {
provider: idP,
},
},
},
where: {
identityProvider: idP as IdentityProvider,
identityProviderId: account.providerAccountId,
},
});
if (existingUser) {
// In this case there's an existing user and their email address
// hasn't changed since they last logged in.
if (existingUser.email === user.email) {
try {
// If old user without Account entry we link their google account
if (existingUser.accounts.length === 0) {
const linkAccountWithUserData = { ...account, userId: existingUser.id };
await calcomAdapter.linkAccount(linkAccountWithUserData);
}
} catch (error) {
if (error instanceof Error) {
console.error("Error while linking account of already existing user");
}
}
if (existingUser.twoFactorEnabled) {
return loginWithTotp(existingUser);
} else {
return true;
}
}
// If the email address doesn't match, check if an account already exists
// with the new email address. If it does, for now we return an error. If
// not, update the email of their account and log them in.
const userWithNewEmail = await prisma.user.findFirst({
where: { email: user.email },
});
if (!userWithNewEmail) {
await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } });
if (existingUser.twoFactorEnabled) {
return loginWithTotp(existingUser);
} else {
return true;
}
} else {
return "/auth/error?error=new-email-conflict";
}
}
// If there's no existing user for this identity provider and id, create
// a new account. If an account already exists with the incoming email
// address return an error for now.
const existingUserWithEmail = await prisma.user.findFirst({
where: { email: user.email },
});
if (existingUserWithEmail) {
// if self-hosted then we can allow auto-merge of identity providers if email is verified
if (!hostedCal && existingUserWithEmail.emailVerified) {
if (existingUserWithEmail.twoFactorEnabled) {
return loginWithTotp(existingUserWithEmail);
} else {
return true;
}
}
// check if user was invited
if (
!existingUserWithEmail.password &&
!existingUserWithEmail.emailVerified &&
!existingUserWithEmail.username
) {
await prisma.user.update({
where: { email: user.email },
data: {
// Slugify the incoming name and append a few random characters to
// prevent conflicts for users with the same name.
username: usernameSlug(user.name),
emailVerified: new Date(Date.now()),
name: user.name,
identityProvider: idP,
identityProviderId: String(user.id),
},
});
if (existingUserWithEmail.twoFactorEnabled) {
return loginWithTotp(existingUserWithEmail);
} else {
return true;
}
}
// 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)
) {
await prisma.user.update({
where: { email: existingUserWithEmail.email },
data: { password: null },
});
if (existingUserWithEmail.twoFactorEnabled) {
return loginWithTotp(existingUserWithEmail);
} else {
return true;
}
} else if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
return "/auth/error?error=use-password-login";
}
return "/auth/error?error=use-identity-login";
}
const newUser = await prisma.user.create({
data: {
// Slugify the incoming name and append a few random characters to
// prevent conflicts for users with the same name.
username: usernameSlug(user.name),
emailVerified: new Date(Date.now()),
name: user.name,
email: user.email,
identityProvider: idP,
identityProviderId: String(user.id),
},
});
const linkAccountNewUserData = { ...account, userId: newUser.id };
await calcomAdapter.linkAccount(linkAccountNewUserData);
if (account.twoFactorEnabled) {
return loginWithTotp(newUser);
} else {
return true;
}
}
return false;
},
async redirect({ url, baseUrl }) {
// Allows relative callback URLs
if (url.startsWith("/")) return `${baseUrl}${url}`;
// Allows callback URLs on the same domain
else if (new URL(url).hostname === new URL(WEBAPP_URL).hostname) return url;
return baseUrl;
},
},
});
export default NextAuth(AUTH_OPTIONS);
+5 -5
View File
@@ -1,14 +1,14 @@
import { IdentityProvider } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { ErrorCode, hashPassword, verifyPassword } from "../../../lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
const session = await getServerSession({ req, res });
if (!session || !session.user || !session.user.email) {
res.status(401).json({ message: "Not authenticated" });
+1 -2
View File
@@ -1,9 +1,8 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import prisma from "@calcom/prisma";
import { hashPassword } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(400).json({ message: "" });
+2 -2
View File
@@ -2,8 +2,8 @@ import { IdentityProvider } from "@prisma/client";
import type { NextApiRequest } from "next";
import z from "zod";
import { isPasswordValid } from "@calcom/lib/auth";
import { hashPassword } from "@calcom/lib/auth";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import slugify from "@calcom/lib/slugify";
+1 -1
View File
@@ -1,7 +1,7 @@
import { IdentityProvider } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/lib/auth";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import slugify from "@calcom/lib/slugify";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
@@ -1,17 +1,18 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { ErrorCode, getSession, verifyPassword } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
@@ -1,17 +1,17 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { ErrorCode, getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
@@ -3,17 +3,18 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import qrcode from "qrcode";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import { symmetricEncrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { ErrorCode, getSession, verifyPassword } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
+2 -3
View File
@@ -1,14 +1,13 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import notEmpty from "@calcom/lib/notEmpty";
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (!session?.user?.id) {
res.status(401).json({ message: "Not authenticated" });
+4 -4
View File
@@ -1,11 +1,11 @@
import type { NextApiRequest } from "next";
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
import { getSession } from "@calcom/lib/auth";
import { defaultResponder } from "@calcom/lib/server";
async function handler(req: NextApiRequest & { userId?: number }) {
const session = await getSession({ req });
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
const session = await getServerSession({ req, res });
/* To mimic API behavior and comply with types */
req.userId = session?.user?.id || -1;
const booking = await handleNewBooking(req, {
+4 -4
View File
@@ -1,11 +1,11 @@
import type { NextApiRequest } from "next";
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
import { getSession } from "@calcom/lib/auth";
import { defaultResponder, defaultHandler } from "@calcom/lib/server";
async function handler(req: NextApiRequest & { userId?: number }) {
const session = await getSession({ req });
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
const session = await getServerSession({ req, res });
/* To mimic API behavior and comply with types */
req.userId = session?.user?.id || -1;
return await handleCancelBooking(req);
+2 -2
View File
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import type { Session } from "next-auth";
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
import { getSession } from "@calcom/lib/auth";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
import prisma from "@calcom/prisma";
@@ -42,7 +42,7 @@ const defaultIntegrationAddHandler = async ({
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Check that user is authenticated
req.session = await getSession({ req });
req.session = await getServerSession({ req, res });
const { args } = req.query;
+2 -2
View File
@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { performance } from "@calcom/lib/server/perfObserver";
let isCold = true;
@@ -9,7 +9,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const prePrismaDate = performance.now();
const prisma = (await import("@calcom/prisma")).default;
const preSessionDate = performance.now();
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (!session) return res.status(409).json({ message: "Unauthorized" });
const preUserDate = performance.now();
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
+7 -2
View File
@@ -3,8 +3,9 @@
*/
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import * as trpcNext from "@calcom/trpc/server/adapters/next";
import { createContext } from "@calcom/trpc/server/createContext";
import { createContext as createTrpcContext } from "@calcom/trpc/server/createContext";
import { appRouter } from "@calcom/trpc/server/routers/_app";
export default trpcNext.createNextApiHandler({
@@ -12,7 +13,11 @@ export default trpcNext.createNextApiHandler({
/**
* @link https://trpc.io/docs/context
*/
createContext,
createContext: ({ req, res }) => {
const sessionGetter = () => getServerSession({ req, res });
return createTrpcContext({ req, res }, sessionGetter);
},
/**
* @link https://trpc.io/docs/error-handling
*/
+1 -1
View File
@@ -2,7 +2,7 @@ import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import prisma from "@calcom/prisma";
import { defaultAvatarSrc } from "@lib/profile";
+4 -4
View File
@@ -1,13 +1,13 @@
import type { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import RoutingFormsRoutingConfig from "@calcom/app-store/ee/routing-forms/pages/app-routing.config";
import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
import type { AppProps } from "@lib/app-providers";
import { getSession } from "@lib/auth";
import { ssrInit } from "@server/lib/ssr";
@@ -103,7 +103,7 @@ export async function getServerSideProps(
appPages?: string[];
}>
) {
const { params } = context;
const { params, req, res } = context;
if (!params) {
return {
notFound: true,
@@ -120,7 +120,7 @@ export async function getServerSideProps(
// appPages is actually hardcoded here and no matter the fileName the same variable would be used.
// We can write some validation logic later on that ensures that [...appPages].tsx file exists
params.appPages = pages.slice(1);
const session = await getSession({ req: context.req });
const session = await getServerSession({ req, res });
const user = session?.user;
const result = await route.getServerSideProps(
+1 -2
View File
@@ -5,7 +5,6 @@ import type { GetStaticPaths, GetStaticPropsContext } from "next";
import path from "path";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import ExisitingGoogleCal from "@calcom/app-store/googlevideo/components/ExistingGoogleCal";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -36,11 +35,11 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
isProOnly={data.isProOnly}
images={source.data?.items as string[] | undefined}
isTemplate={data.isTemplate}
dependencies={data.dependencies}
// tos="https://zoom.us/terms"
// privacy="https://zoom.us/privacy"
body={
<>
{data.slug === "google-meet" && <ExisitingGoogleCal />}
<div dangerouslySetInnerHTML={{ __html: md.render(source.content) }} />
</>
}
+4 -2
View File
@@ -3,8 +3,8 @@ import type { ChangeEventHandler } from "react";
import { useState } from "react";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { classNames } from "@calcom/lib";
import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/client";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
@@ -86,9 +86,11 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
const ssg = await ssgInit(context);
const session = await getSession(context);
const session = await getServerSession({ req, res });
let appStore;
if (session?.user?.id) {
@@ -2,6 +2,7 @@ import type { ResetPasswordRequest } from "@prisma/client";
import debounce from "lodash/debounce";
import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Link from "next/link";
import React, { useMemo } from "react";
@@ -185,6 +186,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
},
id,
csrfToken: await getCsrfToken({ req: context.req }),
...(await serverSideTranslations(context.locale || "en", ["common"])),
},
};
} catch (reason) {
+10 -6
View File
@@ -1,16 +1,16 @@
import debounce from "lodash/debounce";
import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Link from "next/link";
import { useRouter } from "next/router";
import type { SyntheticEvent } from "react";
import React from "react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, EmailField } from "@calcom/ui";
import { getSession } from "@lib/auth";
import AuthContainer from "@components/ui/AuthContainer";
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
@@ -132,17 +132,21 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
);
}
ForgotPassword.getInitialProps = async (context: GetServerSidePropsContext) => {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (session) {
res.writeHead(302, { Location: "/" });
res.end();
return;
return { props: {} };
}
return {
csrfToken: await getCsrfToken(context),
props: {
csrfToken: await getCsrfToken(context),
...(await serverSideTranslations(context.locale || "en", ["common"])),
},
};
};
+5 -3
View File
@@ -9,8 +9,9 @@ import { FormProvider, useForm } from "react-hook-form";
import { FaGoogle } from "react-icons/fa";
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { ErrorCode, getSession } from "@calcom/lib/auth";
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -220,8 +221,9 @@ export default function Login({
// 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 { req, res } = context;
const session = await getServerSession({ req, res });
const ssr = await ssrInit(context);
const verifyJwt = (jwt: string) => {
+5 -3
View File
@@ -4,8 +4,8 @@ import { useRouter } from "next/router";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
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 type { inferSSRProps } from "@calcom/types/inferSSRProps";
@@ -136,10 +136,12 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
const ssr = await ssrInit(context);
const userCount = await prisma.user.count();
const { req } = context;
const session = await getSession({ req });
const session = await getServerSession({ req, res });
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
return {
+5 -2
View File
@@ -1,6 +1,7 @@
import type { GetServerSidePropsContext } from "next";
import { getProviders, signIn, getSession, getCsrfToken } from "next-auth/react";
import { getProviders, signIn, getCsrfToken } from "next-auth/react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { Button } from "@calcom/ui";
type Provider = {
@@ -25,7 +26,9 @@ function signin({ providers }: { providers: Provider[] }) {
export default signin;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const session = await getSession(context);
const { req, res } = context;
const session = await getServerSession({ req, res });
const csrfToken = await getCsrfToken(context);
const providers = await getProviders();
if (session) {
+3 -3
View File
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
import { useEffect } from "react";
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import stripe from "@calcom/features/ee/payments/server/stripe";
import {
hostedCal,
@@ -16,7 +17,6 @@ import { checkUsername } from "@calcom/lib/server/checkUsername";
import prisma from "@calcom/prisma";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import { ssrInit } from "@server/lib/ssr";
@@ -60,9 +60,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
throw new Error(`File is not named sso/[provider]`);
}
const { req } = context;
const { req, res } = context;
const session = await getSession({ req });
const session = await getServerSession({ req, res });
const ssr = await ssrInit(context);
if (session) {
+25 -12
View File
@@ -199,6 +199,10 @@ export default function Success(props: SuccessProps) {
if ((isCancellationMode || changes) && typeof window !== "undefined") {
window.scrollTo(0, document.body.scrollHeight);
}
const tz =
(isSuccessBookingPage
? props.bookingInfo.attendees.find((attendee) => attendee.email === email)?.timeZone
: props.bookingInfo.eventType?.timeZone || props.bookingInfo.user?.timeZone) || timeZone();
const location = props.bookingInfo.location as ReturnType<typeof getEventLocationValue>;
@@ -246,6 +250,7 @@ export default function Success(props: SuccessProps) {
eventName: (props.dynamicEventName as string) || props.eventType.eventName,
host: props.profile.name || "Nameless",
location: location,
bookingFields: bookingInfo.responses,
t,
};
@@ -477,6 +482,7 @@ export default function Success(props: SuccessProps) {
date={dayjs(formerTime)}
is24h={is24h}
isCancelled={isCancelled}
tz={tz}
/>
</p>
)}
@@ -488,6 +494,7 @@ export default function Success(props: SuccessProps) {
date={date}
is24h={is24h}
isCancelled={isCancelled}
tz={tz}
/>
</div>
{(bookingInfo?.user || bookingInfo?.attendees) && (
@@ -785,6 +792,7 @@ type RecurringBookingsProps = {
is24h: boolean;
allRemainingBookings: boolean;
isCancelled: boolean;
tz: string;
};
export function RecurringBookings({
@@ -795,6 +803,7 @@ export function RecurringBookings({
allRemainingBookings,
is24h,
isCancelled,
tz,
}: RecurringBookingsProps) {
const [moreEventsVisible, setMoreEventsVisible] = useState(false);
const {
@@ -822,12 +831,12 @@ export function RecurringBookings({
{eventType.recurringEvent?.count &&
recurringBookingsSorted.slice(0, 4).map((dateStr: string, idx: number) => (
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
{formatToLocalizedDate(dayjs.tz(dateStr, timeZone()), language, "full")}
{formatToLocalizedDate(dayjs.tz(dateStr, tz), language, "full", tz)}
<br />
{formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h)} -{" "}
{formatToLocalizedTime(dayjs(dateStr).add(duration, "m"), language, undefined, !is24h)}{" "}
{formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h, tz)} -{" "}
{formatToLocalizedTime(dayjs(dateStr).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
<span className="text-bookinglight">
({formatToLocalizedTimezone(dayjs(dateStr), language, timeZone())})
({formatToLocalizedTimezone(dayjs(dateStr), language, tz)})
</span>
</div>
))}
@@ -842,12 +851,12 @@ export function RecurringBookings({
{eventType.recurringEvent?.count &&
recurringBookingsSorted.slice(4).map((dateStr: string, idx: number) => (
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
{formatToLocalizedDate(dayjs.tz(date, timeZone()), language, "full")}
{formatToLocalizedDate(dayjs.tz(date, tz), language, "full", tz)}
<br />
{formatToLocalizedTime(date, language, undefined, !is24h)} -{" "}
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h)}{" "}
{formatToLocalizedTime(date, language, undefined, !is24h, tz)} -{" "}
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
<span className="text-bookinglight">
({formatToLocalizedTimezone(dayjs(dateStr), language, timeZone())})
({formatToLocalizedTimezone(dayjs(dateStr), language, tz)})
</span>
</div>
))}
@@ -860,11 +869,11 @@ export function RecurringBookings({
return (
<div className={classNames(isCancelled ? "line-through" : "")}>
{formatToLocalizedDate(dayjs.tz(date, timeZone()), language, "full")}
{formatToLocalizedDate(date, language, "full", tz)}
<br />
{formatToLocalizedTime(date, language, undefined, !is24h)} -{" "}
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h)}{" "}
<span className="text-bookinglight">({formatToLocalizedTimezone(date, language, timeZone())})</span>
{formatToLocalizedTime(date, language, undefined, !is24h, tz)} -{" "}
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
<span className="text-bookinglight">({formatToLocalizedTimezone(date, language, tz)})</span>
</div>
);
}
@@ -901,6 +910,7 @@ const getEventTypesFromDB = async (id: number) => {
currency: true,
bookingFields: true,
disableGuests: true,
timeZone: true,
owner: {
select: userSelect,
},
@@ -1019,12 +1029,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
name: true,
email: true,
username: true,
timeZone: true,
},
},
attendees: {
select: {
name: true,
email: true,
timeZone: true,
},
},
eventTypeId: true,
@@ -1032,6 +1044,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
select: {
eventName: true,
slug: true,
timeZone: true,
},
},
},
+37 -32
View File
@@ -2,7 +2,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod";
import type { PeriodType } from "@prisma/client";
import { SchedulingType } from "@prisma/client";
import type { SchedulingType } from "@prisma/client";
import type { GetServerSidePropsContext } from "next";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -10,22 +10,23 @@ import { z } from "zod";
import { validateCustomEventName } from "@calcom/core/event";
import type { EventLocationType } from "@calcom/core/location";
import { validateBookingLimitOrder } from "@calcom/lib";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { validateIntervalLimitOrder } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
import getEventTypeById from "@calcom/lib/getEventTypeById";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar";
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
import { Form, showToast } from "@calcom/ui";
import { asStringOrThrow } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
@@ -85,9 +86,9 @@ export type FormValues = {
externalId: string;
};
successRedirectUrl: string;
bookingLimits?: BookingLimit;
hosts: { userId: number }[];
hostsFixed: { userId: number }[];
durationLimits?: IntervalLimit;
bookingLimits?: IntervalLimit;
hosts: { userId: number; isFixed: boolean }[];
bookingFields: z.infer<typeof eventTypeBookingFields>;
};
@@ -181,6 +182,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
delete metadata.config?.useHostSchedulesForTeamEvent;
}
const bookingFields: Prisma.JsonObject = {};
eventType.bookingFields.forEach(({ name }) => {
bookingFields[name] = name;
});
const defaultValues = {
title: eventType.title,
locations: eventType.locations || [],
@@ -188,6 +195,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
description: eventType.description ?? undefined,
schedule: eventType.schedule || undefined,
bookingLimits: eventType.bookingLimits || undefined,
durationLimits: eventType.durationLimits || undefined,
length: eventType.length,
hidden: eventType.hidden,
periodDates: {
@@ -200,16 +208,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
schedulingType: eventType.schedulingType,
minimumBookingNotice: eventType.minimumBookingNotice,
metadata,
hosts: !!eventType.hosts?.length
? eventType.hosts.filter((host) => !host.isFixed)
: eventType.users
.filter(() => eventType.schedulingType === SchedulingType.ROUND_ROBIN)
.map((user) => ({ userId: user.id })),
hostsFixed: !!eventType.hosts?.length
? eventType.hosts.filter((host) => host.isFixed)
: eventType.users
.filter(() => eventType.schedulingType === SchedulingType.COLLECTIVE)
.map((user) => ({ userId: user.id })),
hosts: eventType.hosts,
} as const;
const formMethods = useForm<FormValues>({
@@ -221,9 +220,13 @@ const EventTypePage = (props: EventTypeSetupProps) => {
// Make it optional because it's not submitted from all tabs of the page
eventName: z
.string()
.refine((val) => validateCustomEventName(val, t("invalid_event_name_variables")) === true, {
message: t("invalid_event_name_variables"),
})
.refine(
(val) =>
validateCustomEventName(val, t("invalid_event_name_variables"), bookingFields) === true,
{
message: t("invalid_event_name_variables"),
}
)
.optional(),
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
bookingFields: eventTypeBookingFields,
@@ -302,12 +305,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
seatsPerTimeSlot,
seatsShowAttendees,
bookingLimits,
durationLimits,
recurringEvent,
locations,
metadata,
customInputs,
hosts: hostsInput,
hostsFixed,
// We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars
seatsPerTimeSlotEnabled,
@@ -316,14 +318,14 @@ const EventTypePage = (props: EventTypeSetupProps) => {
...input
} = values;
const hosts: ((typeof hostsInput)[number] & { isFixed?: boolean })[] = [];
if (hostsInput || hostsFixed) {
hosts.push(...hostsInput.concat(hostsFixed.map((host) => ({ isFixed: true, ...host }))));
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
}
if (bookingLimits) {
const isValid = validateBookingLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
if (durationLimits) {
const isValid = validateIntervalLimitOrder(durationLimits);
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
}
if (metadata?.multipleDuration !== undefined) {
@@ -338,7 +340,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
updateMutation.mutate({
...input,
hosts,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
@@ -348,6 +349,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
beforeEventBuffer: beforeBufferTime,
afterEventBuffer: afterBufferTime,
bookingLimits,
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,
metadata,
@@ -369,8 +371,10 @@ const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) =
};
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, query } = context;
const session = await getSession({ req });
const { req, res, query } = context;
const session = await getServerSession({ req, res });
const typeParam = parseInt(asStringOrThrow(query.type));
const ssr = await ssrInit(context);
@@ -391,9 +395,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
try {
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma });
return {
props: {
session,
// session,
type: typeParam,
trpcState: ssr.dehydrate(),
initialData: {
+1 -1
View File
@@ -338,7 +338,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<MemoizedItem type={type} group={group} readOnly={readOnly} />
<div className="mt-4 hidden sm:mt-0 sm:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
{type.users?.length > 1 && (
{type.team && (
<AvatarGroup
className="relative top-1 right-3"
size="sm"
@@ -4,7 +4,7 @@ import Head from "next/head";
import { useRouter } from "next/router";
import { z } from "zod";
import { getSession } from "@calcom/lib/auth";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
@@ -140,8 +140,10 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
};
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
const crypto = await import("crypto");
const session = await getSession(context);
const session = await getServerSession({ req, res });
if (!session?.user?.id) {
return { redirect: { permanent: false, destination: "/auth/login" } };
+5 -4
View File
@@ -1,13 +1,14 @@
import type { NextPageContext } from "next";
import type { GetServerSidePropsContext } from "next";
import { getSession } from "@lib/auth";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
function RedirectPage() {
return;
}
export async function getServerSideProps(context: NextPageContext) {
const session = await getSession(context);
export async function getServerSideProps({ req, res }: GetServerSidePropsContext) {
const session = await getServerSession({ req, res });
if (!session?.user?.id) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
@@ -69,7 +69,10 @@ const ConferencingLayout = () => {
}, []);
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
onSuccess: onSuccessCallback,
onSuccess: async () => {
await utils.viewer.getUsersDefaultConferencingApp.invalidate();
onSuccessCallback();
},
onError: (error) => {
showToast(`Error: ${error.message}`, "error");
},
@@ -5,8 +5,8 @@ import type { BaseSyntheticEvent } from "react";
import { useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { ErrorCode } from "@calcom/lib/auth";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
@@ -2,8 +2,8 @@ import { IdentityProvider } from "@prisma/client";
import { signOut, useSession } from "next-auth/react";
import { useForm } from "react-hook-form";
import { identityProviderNameMap } from "@calcom/features/auth/lib/identityProviderNameMap";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
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";
+25 -23
View File
@@ -7,7 +7,7 @@ import { useEffect } from "react";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
import { CAL_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { md } from "@calcom/lib/markdownIt";
@@ -48,29 +48,31 @@ function TeamPage({ team }: TeamPageProps) {
"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",
!isEmbed && "bg-white"
)}>
<Link
href={`/team/${team.slug}/${type.slug}`}
className="flex justify-between px-6 py-4"
data-testid="event-type-link">
<div className="flex-shrink">
<div className="flex flex-wrap items-center space-x-2 rtl:space-x-reverse">
<h2 className="dark:text-darkgray-700 text-sm font-semibold text-gray-700">{type.title}</h2>
<div className="px-6 py-4 ">
<Link
href={`/team/${team.slug}/${type.slug}`}
data-testid="event-type-link"
className="flex justify-between">
<div className="flex-shrink">
<div className="flex flex-wrap items-center space-x-2 rtl:space-x-reverse">
<h2 className="dark:text-darkgray-700 text-sm font-semibold text-gray-700">{type.title}</h2>
</div>
<EventTypeDescription className="text-sm" eventType={type} />
</div>
<EventTypeDescription className="text-sm" eventType={type} />
</div>
<div className="mt-1 self-center">
<AvatarGroup
truncateAfter={4}
className="flex flex-shrink-0"
size="sm"
items={type.users.map((user) => ({
alt: user.name || "",
title: user.name || "",
image: CAL_URL + "/" + user.username + "/avatar.png" || "",
}))}
/>
</div>
</Link>
<div className="mt-1 self-center">
<AvatarGroup
truncateAfter={4}
className="flex flex-shrink-0"
size="sm"
items={type.users.map((user) => ({
alt: user.name || "",
title: user.name || "",
image: CAL_URL + "/" + user.username + "/avatar.png" || "",
}))}
/>
</div>
</Link>
</div>
</li>
))}
</ul>
+5 -2
View File
@@ -1,9 +1,9 @@
import DailyIframe from "@daily-co/daily-js";
import type { GetServerSidePropsContext } from "next";
import { getSession } from "next-auth/react";
import Head from "next/head";
import { useEffect } from "react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
@@ -82,6 +82,8 @@ export default function JoinCall(props: JoinCallPageProps) {
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { req, res } = context;
const ssr = await ssrInit(context);
const booking = await prisma.booking.findUnique({
@@ -139,7 +141,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
startTime: booking.startTime.toString(),
endTime: booking.endTime.toString(),
});
const session = await getSession();
const session = await getServerSession({ req, res });
// set meetingPassword to null for guests
if (session?.user.id !== bookingObj.user?.id) {
@@ -1472,7 +1472,7 @@
"add_an_option": "إضافة خيار",
"radio": "الراديو",
"google_meet_warning": "من أجل استخدام Google Meet، يجب عليك تعيين تقويم وجهتك على تقويم Google",
"individual": "فردي",
"individual": "فرد",
"all_bookings_filter_label": "جميع الحجوزات",
"all_users_filter_label": "جميع المستخدمين",
"your_bookings_filter_label": "الحجوزات لديك",
@@ -1491,13 +1491,13 @@
"date_overrides_update_btn": "تحديث التجاوز",
"event_type_duplicate_copy_text": "{{slug}}-نسخ",
"set_as_default": "تعيين كافتراضي",
"hide_eventtype_details": "إخفاء تفاصيل نوع الفعالية",
"hide_eventtype_details": "إخفاء تفاصيل EventType",
"show_navigation": "إظهار التنقل",
"hide_navigation": "إخفاء التنقل",
"verification_code_sent": "تم إرسال رمز التحقق",
"verified_successfully": "تم التحقق بنجاح",
"wrong_code": "رمز تحقق خاطئ",
"not_verified": "لم يتم التحقق بعد",
"wrong_code": "رمز التحقق غير صحيح",
"not_verified": "لم يتم التحقق منه بعد",
"no_availability_in_month": "غير متاح في شهر {{month}}",
"view_next_month": "عرض الشهر التالي",
"send_code": "إرسال الرمز",
+119 -1
View File
@@ -7,6 +7,7 @@
"upgrade_now": "Přejít na vyšší tarif",
"accept_invitation": "Přijmout pozvánku",
"calcom_explained": "{{appName}} je open source alternativa, která vám dává kontrolu nad vlastními daty, workflow a vzhledem.",
"calcom_explained_new_user": "Dokončete nastavení svého účtu {{appName}}! Jste jen několik kroků od vyřešení všech svých problémů s plánováním.",
"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: {{title}} v {{date}}",
@@ -23,6 +24,8 @@
"rejection_reason_description": "Opravdu chcete rezervaci odmítnout? Osobě, která se váš čas pokusila rezervovat, dáme vědět. Níž můžete uvést důvod.",
"rejection_confirmation": "Odmítnout rezervaci",
"manage_this_event": "Spravovat tuto událost",
"invite_team_member": "Pozvat člena týmu",
"invite_team_notifcation_badge": "Pozv.",
"your_event_has_been_scheduled": "Vaše událost byla naplánována",
"your_event_has_been_scheduled_recurring": "Opakovaná událost byla naplánována",
"accept_our_license": "Přijměte naši licenci změnou proměnné .env <1>NEXT_PUBLIC_LICENSE_CONSENT</1> na '{{agree}}'.",
@@ -410,6 +413,8 @@
"password_updated_successfully": "Heslo bylo úspěšně změněno",
"password_has_been_changed": "Vaše heslo bylo úspěšně změněno.",
"error_changing_password": "Chyba při změně hesla",
"session_timeout_changed": "Konfigurace vaší relace byla aktualizována.",
"session_timeout_change_error": "Chyba při aktualizaci konfigurace relace",
"something_went_wrong": "Někde došlo k chybě.",
"something_doesnt_look_right": "Něco nefunguje správně?",
"please_try_again": "Zkuste to prosím znovu.",
@@ -428,6 +433,7 @@
"password_hint_num": "Obsahuje alespoň 1 číslo",
"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é.",
"incorrect_username_password": "Uživatelské jméno nebo heslo je nesprávné.",
"24_h": "24h",
"use_setting": "Použít nastavení",
"am_pm": "am/pm",
@@ -455,6 +461,7 @@
"slots_load_fail": "Nepodařilo se načíst dostupné časové sloty.",
"additional_guests": "Přidat hosty",
"your_name": "Vaše jméno",
"your_full_name": "Vaše celé jméno",
"email_address": "E-mailová adresa",
"enter_valid_email": "Zadejte platný e-mail",
"location": "Místo",
@@ -508,6 +515,8 @@
"admin": "Administrátor",
"administrator_user": "Uživatel administrátor",
"lets_create_first_administrator_user": "Pojďme vytvořit prvního uživatele administrátora.",
"admin_user_created": "Nastavení uživatele administrátora",
"admin_user_created_description": "Uživatele administrátora již máte vytvořeného. Teď se můžete přihlásit ke svému účtu.",
"new_member": "Nový člen",
"invite": "Pozvat",
"add_team_members": "Přidat členy týmu",
@@ -657,6 +666,7 @@
"edit_availability": "Upravit dostupnost",
"configure_availability": "Nastavit časy, kdy jste dostupní pro rezervace.",
"copy_times_to": "Kopírovat časy do",
"copy_times_to_tooltip": "Zkopírovat časy do…",
"change_weekly_schedule": "Změnit týdenní pracovní dobu",
"logo": "Logo",
"error": "Chyba",
@@ -668,6 +678,7 @@
"add_attendees": "Přidat účastníky",
"show_advanced_settings": "Zobrazit pokročilá nastavení",
"event_name": "Název události",
"event_name_in_calendar": "Název události v kalendáři",
"event_name_tooltip": "Název, který se zobrazí v kalendářích",
"meeting_with_user": "Schůzka s {{attendeeName}}",
"additional_inputs": "Další pole",
@@ -786,6 +797,7 @@
"number_apps_one": "Počet aplikací: {{count}}",
"number_apps_other": "Počet aplikací: {{count}}",
"trending_apps": "Populární aplikace",
"most_popular": "Nejoblíbenější",
"explore_apps": "Aplikace v kategorii {{category}}",
"installed_apps": "Nainstalované aplikace",
"free_to_use_apps": "Zdarma",
@@ -796,12 +808,14 @@
"no_category_apps_description_analytics": "Přidejte analytickou aplikaci pro vaše rezervační stránky",
"no_category_apps_description_automation": "Přidejte aplikaci pro automatizaci",
"no_category_apps_description_other": "Přidejte jakýkoli jiný typ aplikace pro nejrůznější činnosti",
"no_category_apps_description_web3": "Přidejte aplikaci web3 pro vaše rezervační stránky",
"installed_app_calendar_description": "Nastavte si kalendáře, ať můžete kontrolovat konflikty a zabránit tak dvojím rezervacím.",
"installed_app_conferencing_description": "Přidejte své oblíbené aplikace pro videokonference pro vaše schůzky",
"installed_app_payment_description": "Nakonfigurujte služby zpracování plateb, které se mají používat při strhávání plateb od klientů.",
"installed_app_analytics_description": "Nakonfigurujte aplikace, které chcete použít pro své rezervační stránky",
"installed_app_other_description": "Všechny vaše nainstalované aplikace z ostatních kategorií.",
"installed_app_automation_description": "Konfigurovat aplikace pro automatizaci",
"installed_app_web3_description": "Nakonfigurujte aplikace web3, které se mají použít pro vaše rezervační stránky",
"analytics": "Analytické nástroje",
"empty_installed_apps_headline": "Nenainstalovány žádné aplikace",
"empty_installed_apps_description": "Aplikace vám umožní zlepšit pracovní postupy a výrazně zkvalitnit plánování.",
@@ -929,6 +943,11 @@
"current_location": "Současné místo",
"user_phone": "Vaše telefonní číslo",
"new_location": "Nové místo",
"session": "Relace",
"session_description": "Mějte pod kontrolou relaci svého účtu",
"session_timeout_after": "Časový limit relace vyprší po",
"session_timeout": "Časový limit relace",
"session_timeout_description": "Po uplynutí určitého času se relace zneplatní.",
"no_location": "Není definováno žádné místo",
"set_location": "Nastavit místo",
"update_location": "Aktualizovat místo",
@@ -1072,6 +1091,7 @@
"broken_video_action": "Nelze přidat odkaz na schůzku <1>{{location}}</1> k vaší plánované události. Kontaktujte pozvané nebo aktualizujte událost ve svém kalendáři a přidejte podrobnosti. Můžete buď <3>změnit místo u typu události</3>, nebo zkusit <5>aplikaci odstranit a znovu přidat.</5>",
"broken_calendar_action": "Aktualizace vašeho kalendáře <1>{{calendar}}</1> se nezdařila. <2> Zkontrolujte si nastavení kalendáře nebo kalendář odeberte a zase přidejte </2>",
"attendee_name": "Jméno účastníka",
"scheduler_full_name": "Celé jméno osoby provádějící rezervaci",
"broken_integration": "Narušená integrace",
"problem_adding_video_link": "Při přidávání odkazu na video došlo k chybě",
"problem_updating_calendar": "Při aktualizaci kalendáře došlo k chybě",
@@ -1214,6 +1234,7 @@
"connect_automation_apps": "Připojit aplikace pro automatizaci",
"connect_analytics_apps": "Připojit analytické aplikace",
"connect_other_apps": "Propojit další aplikace",
"connect_web3_apps": "Připojení aplikací web3",
"current_step_of_total": "Krok {{currentStep}} / {{maxSteps}}",
"add_variable": "Přidat proměnnou",
"custom_phone_number": "Vlastní telefonní číslo",
@@ -1230,6 +1251,7 @@
"to": "Komu",
"workflow_turned_on_successfully": "Pracovní postup {{workflowName}} byl {{offOn}}",
"download_responses": "Stáhnout odpovědi",
"download_responses_description": "Stáhněte si všechny odpovědi do formuláře ve formátu CSV.",
"download": "Stáhnout",
"create_your_first_form": "Vytvořte svůj první formulář",
"create_your_first_form_description": "Pomocí adresných formulářů můžete klást kvalifikační otázky a adresovat je správné osobě nebo typu události.",
@@ -1271,6 +1293,8 @@
"routing_forms_send_email_owner": "Odeslat e-mail vlastníkovi",
"routing_forms_send_email_owner_description": "Po odeslání formuláře bude vlastníkovi odeslán e-mail",
"add_new_form": "Přidat nový formulář",
"create_your_first_route": "Vytvořte svoje první směrování",
"route_to_the_right_person": "Nasměrovat té správné osobě na základě odpovědí ve vašem formuláři",
"form_description": "Vytvořte vlastní formulář, který adresujete rezervující osobě",
"copy_link_to_form": "Kopírovat odkaz na formulář",
"theme": "Motiv",
@@ -1301,6 +1325,7 @@
"password_reset_leading": "Pokud brzy neobdržíte e-mail, zkontrolujte, zda je zadaná e-mailová adresa správná, dále složku nevyžádané pošty nebo se obraťte na podporu, pokud problém přetrvává.",
"password_updated": "Heslo bylo úspěšně změněno!",
"pending_payment": "Čeká se na platbu",
"pending_invites": "Čekající pozvánky",
"confirmation_page_rainbow": "Zamkněte svoji událost pomocí tokenů nebo NFT na platformách Ethereum, Polygon a dalších.",
"not_on_cal": "Nenachází se na {{appName}}",
"no_calendar_installed": "Není nainstalován kalendář",
@@ -1427,6 +1452,9 @@
"disabled_calendar": "Pokud máte nainstalovaný jiný kalendář, budou do něj přidány nové rezervace. Pokud ne, připojte nový kalendář, abyste nepřišli o žádné nové rezervace.",
"enable_apps": "Povolit aplikace",
"enable_apps_description": "Povolte aplikace, které mohou uživatelé integrovat s Cal.com",
"purchase_license": "Kupte si licenci",
"already_have_key": "Klíč již mám:",
"already_have_key_suggestion": "Zkopírujte sem svou stávající proměnnou prostředí CALCOM_LICENSE_KEY.",
"app_is_enabled": "Aplikace {{appName}} je povolena",
"app_is_disabled": "Aplikace {{appName}} je zakázána",
"keys_have_been_saved": "Klíče byly uloženy",
@@ -1447,6 +1475,7 @@
"individual": "Jedinec",
"all_bookings_filter_label": "Všechny rezervace",
"all_users_filter_label": "Všichni uživatelé",
"your_bookings_filter_label": "Vaše rezervace",
"meeting_url_variable": "Adresa URL schůzky",
"meeting_url_info": "Adresa URL konferenční schůzky k události",
"date_overrides": "Změna dnů",
@@ -1485,6 +1514,7 @@
"round_robin_hosts": "Hostitelé typu „round robin“",
"minimum_round_robin_hosts_count": "Počet hostitelů, kteří se musí zúčastnit",
"hosts": "Hostitelé",
"upgrade_to_enable_feature": "K povolení této funkce je třeba vytvořit tým. Tým vytvoříte kliknutím.",
"new_attendee": "Nový účastník",
"awaiting_approval": "Čeká na schválení",
"requires_google_calendar": "Tato aplikace vyžaduje připojení ke Kalendáři Google",
@@ -1493,9 +1523,97 @@
"continue_to_install_google_calendar": "Pokračovat v instalaci Kalendáře Google",
"install_google_meet": "Nainstalovat Google Meet",
"install_google_calendar": "Nainstalovat Kalendář Google",
"sender_name": "Jméno odesílatele",
"already_invited": "Účastník je již pozvaný",
"no_recordings_found": "Nenalezeny žádné nahrávky",
"reporting": "Hlášení",
"reporting_feature": "Zobrazení všech příchozích dat a jejich stažení ve formátu CSV",
"teams_plan_required": "Potřebujete tarif Teams",
"routing_forms_are_a_great_way": "Směrovací formuláře představují skvělý způsob, jak příchozí potenciální kontakty nasměrovat na tu správnou osobu. Přístup k této funkci získáte po upgradu na tarif Teams.",
"choose_a_license": "Vyberte si licenci",
"choose_license_description": "Cal.com se dodává s dostupnou a bezplatnou licencí AGPLv3, která má určitá omezení. Kdykoli je ale možné provést upgrade na licenci Enterprise. Upgrade je možné provést kdykoli v pozdější době.",
"license": "Licence",
"agplv3_license": "Licence AGPLv3",
"ee_enterprise_license": "„/ee“ Licence Enterprise",
"enterprise_booking_fee": "Od {{{enterprise_booking_fee}}/měsíc",
"enterprise_license_includes": "Vše pro komerční použití",
"no_need_to_keep_your_code_open_source": "Kód nemusíte nechávat jako open-source",
"repackage_rebrand_resell": "Snadné přebalení, rebranding a přeprodej",
"a_vast_suite_of_enterprise_features": "Rozsáhlá sada firemních funkcí",
"free_license_fee": "0,00 $/měsíc",
"forever_open_and_free": "Navždy otevřený a zdarma",
"required_to_keep_your_code_open_source": "Svůj kód musíte nechat jako open-source",
"cannot_repackage_and_resell": "Přebalení, rebranding a přeprodej nejsou tak snadné",
"no_enterprise_features": "Žádné firemní funkce",
"step_enterprise_license": "Licence Enterprise",
"step_enterprise_license_description": "Vše pro komerční použití s privátním hostingem, přebalováním, rebrandingem a přeprodejem a přístupem k exkluzivním firemním komponentám.",
"setup": "Nastavení",
"setup_description": "Nastavte instanci Cal.com",
"configure": "Konfigurovat",
"sso_configuration": "Single Sign-On",
"sso_configuration_description": "Konfigurace SAML/OIDC SSO a umožnění členům týmu přihlašování prostřednictvím poskytovatele identity",
"sso_oidc_heading": "SSO s OIDC",
"sso_oidc_description": "Konfigurace OIDC SSO s poskytovatelem identity podle vlastního výběru.",
"sso_oidc_configuration_title": "Konfigurace OIDC",
"sso_oidc_configuration_description": "Konfigurace připojení OIDC k poskytovateli identity. Potřebné informace najdete u svého poskytovatele identity.",
"sso_oidc_callback_copied": "Adresa URL zpětného volání zkopírována",
"sso_saml_heading": "SSO s protokolem SAML",
"sso_saml_description": "Konfigurace SSO SAML s vybraným poskytovatelem identity.",
"sso_saml_configuration_title": "Konfigurace SAML",
"sso_saml_configuration_description": "Konfigurace připojení SAML k vašemu poskytovateli identity. Potřebné informace najdete u svého poskytovatele identity.",
"sso_saml_acsurl_copied": "Adresa URL ACS zkopírována",
"sso_saml_entityid_copied": "ID subjektu zkopírováno",
"sso_connection_created_successfully": "Konfigurace {{connectionType}} vytvořena",
"sso_connection_deleted_successfully": "Konfigurace {{connectionType}} odstraněna",
"delete_sso_configuration": "Odstranit konfiguraci {{connectionType}}",
"delete_sso_configuration_confirmation": "Ano, odstranit konfiguraci {{connectionType}}",
"delete_sso_configuration_confirmation_description": "Opravdu chcete odstranit konfiguraci {{connectionType}}? Členové vašeho týmu, kteří používají přihlášení {{connectionType}}, ztratí přístup k webu Cal.com.",
"organizer_timezone": "Časové pásmo organizátora",
"email_no_user_cta": "Vytvořit účet",
"email_user_cta": "Zobrazit pozvánku",
"email_no_user_invite_heading": "Byli jste pozváni do týmu v aplikaci {{appName}}",
"email_no_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k jeho týmu v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu týmu umožňuje plánovat schůzky bez e-mailového ping-pongu.",
"email_no_user_invite_steps_intro": "Provedeme vás několika krátkými kroky a za malou chvíli si budete se svým týmem užívat plánování bez stresu.",
"email_no_user_step_one": "Vyberte si uživatelské jméno",
"email_no_user_step_two": "Připojte svůj účet kalendáře",
"email_no_user_step_three": "Nastavte si dostupnost",
"email_no_user_step_four": "Připojit se k týmu {{teamName}}",
"email_no_user_signoff": "Tým {{appName}} vám přeje ničím nerušené plánování",
"impersonation_user_tip": "Chystáte se zosobnit uživatele, což znamená, že můžete provádět změny jeho jménem. Počínejte si obezřetně, prosím.",
"available_variables": "Dostupné proměnné",
"scheduler": "{Scheduler}",
"recommended_next_steps": "Doporučené další kroky",
"create_a_managed_event": "Vytvořte typ spravované události",
"meetings_are_better_with_the_right": "Schůzky jsou lepší, když jsou na nich přítomni ti správní členové týmu. Pozvěte je.",
"collective_or_roundrobin": "Kolektivní nebo round-robin",
"book_your_team_members": "Rezervujte si členy týmu všechny spolu pomocí kolektivních událostí nebo postupně pomocí round-robinu.",
"default_app_link_title": "Nastavte výchozí odkaz na aplikaci",
"default_app_link_description": "Nastavení výchozího odkazu na aplikaci umožní, aby všechny nově vytvořené typy událostí používaly vámi nastavený odkaz na aplikaci.",
"change_default_conferencing_app": "Nastavit jako výchozí",
"booking_confirmation_failed": "Potvrzení rezervace se nezdařilo"
"under_maintenance": "Odstávka z důvodu údržby",
"under_maintenance_description": "Tým {{appName}} provádí plánovanou údržbu. V případě dotazů se obraťte na podporu.",
"event_type_seats": "Počet míst: {{numberOfSeats}}",
"booking_with_payment_cancelled": "Platba za tuto událost již není možná",
"booking_with_payment_cancelled_already_paid": "Vrácení peněz za tuto rezervační platbu je na cestě.",
"booking_with_payment_cancelled_refunded": "Tato rezervační platba byla vrácena.",
"booking_confirmation_failed": "Potvrzení rezervace se nezdařilo",
"get_started_zapier_templates": "Začněte používat šablony Zapier",
"a_routing_form": "Směrovací formulář",
"form_description_placeholder": "Popis formuláře",
"keep_me_connected_with_form": "Udržujte se mnou spojení pomocí formuláře",
"fields_in_form_duplicated": "Veškeré změny ve směrování a polích duplikovaného formuláře s promítnou duplicitě.",
"form_deleted": "Formulář odstraněn",
"delete_form_action": "Ano, odstranit formulář",
"typeform_redirect_url_copied": "Adresa URL přesměrování Typeform zkopírována! Můžete jít nastavit adresu URL ve formuláři Typeform.",
"modifications_in_fields_warning": "Úpravy v polích a směrování následujících formulářů se projeví v tomto formuláři.",
"connected_forms": "Připojené formuláře",
"form_modifications_warning": "Úprava polí a tras na tomto místě by se dotkla následujících formulářů.",
"responses_collection_waiting_description": "Počkejte chvíli na shromáždění odpovědí. Formulář můžete odeslat také sami.",
"this_is_what_your_users_would_see": "Toto uvidí vaši uživatelé",
"identifies_name_field": "Identifikuje pole podle tohoto názvu.",
"add_1_option_per_line": "Přidejte 1 možnost na řádek",
"select_a_router": "Vyberte směrování",
"add_a_new_route": "Přidejte nové směrování",
"no_responses_yet": "Zatím žádné odpovědi",
"this_will_be_the_placeholder": "Toto bude zástupný znak"
}
+14 -5
View File
@@ -1156,7 +1156,8 @@
"event_advanced_tab_title": "Advanced",
"event_setup_multiple_duration_error": "Event Setup: Multiple durations requires at least 1 option.",
"event_setup_multiple_duration_default_error": "Event Setup: Please select a valid default duration.",
"event_setup_booking_limits_error": "Booking limits must be in accending order. [day,week,month,year]",
"event_setup_booking_limits_error": "Booking limits must be in ascending order. [day, week, month, year]",
"event_setup_duration_limits_error": "Duration limits must be in ascending order. [day, week, month, year]",
"select_which_cal": "Select which calendar to add bookings to",
"custom_event_name": "Custom event name",
"custom_event_name_description": "Create customised event names to display on calendar event",
@@ -1252,7 +1253,7 @@
"attendee_name_info": "The person booking's name",
"to": "To",
"workflow_turned_on_successfully": "{{workflowName}} workflow turned {{offOn}} successfully",
"download_responses": "Download Responses",
"download_responses": "Download responses",
"download_responses_description": "Download all responses to your form in CSV format.",
"download": "Download",
"create_your_first_form": "Create your first form",
@@ -1291,7 +1292,7 @@
"exchange_authentication_standard": "Basic authentication",
"exchange_authentication_ntlm": "NTLM authentication",
"exchange_compression": "GZip compression",
"routing_forms_description": "You can see all forms and routes you have created here.",
"routing_forms_description": "Create forms to direct attendees to the correct destinations",
"routing_forms_send_email_owner": "Send Email to Owner",
"routing_forms_send_email_owner_description": "Sends an email to the owner when the form is submitted",
"add_new_form": "Add new form",
@@ -1340,6 +1341,8 @@
"report_app": "Report app",
"limit_booking_frequency": "Limit booking frequency",
"limit_booking_frequency_description": "Limit how many times this event can be booked",
"limit_total_booking_duration": "Limit total booking duration",
"limit_total_booking_duration_description": "Limit total amount of time that this event can be booked",
"add_limit": "Add Limit",
"team_name_required": "Team name required",
"show_attendees": "Share attendee information between guests",
@@ -1443,6 +1446,7 @@
"calcom_is_better_with_team": "Cal.com is better with teams",
"add_your_team_members": "Add your team members to your event types. Use collective scheduling to include everyone or find the most suitable person with round robin scheduling.",
"booking_limit_reached": "Booking Limit for this event type has been reached",
"duration_limit_reached": "Duration Limit for this event type has been reached",
"admin_has_disabled": "An admin has disabled {{appName}}",
"disabled_app_affects_event_type": "An admin has disabled {{appName}} which affects your event type {{eventType}}",
"disable_payment_app": "The admin has disabled {{appName}} which affects your event type {{title}}. Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin renables your payment method.",
@@ -1635,10 +1639,15 @@
"add_a_new_route": "Add a new Route",
"no_responses_yet": "No responses yet",
"this_will_be_the_placeholder": "This will be the placeholder",
"this_app_requires_connected_account": "{{appName}} requires a connected {{dependencyName}} account",
"connect_app": "Connect {{dependencyName}}",
"app_is_connected": "{{dependencyName}} is connected",
"requires_app": "Requires {{dependencyName}}",
"verification_code": "Verification code",
"can_you_try_again": "Can you try again with a different time?",
"verify": "Verify",
"invalid_event_name_variables":"There is an invalid variable in your event name",
"select_all": "Select All",
"default_conferencing_bulk_title": "Bulk update existing event types",
"default_conferencing_bulk_description": "Update the locations for the selected event types",
"invalid_event_name_variables": "There is an invalid variable in your event name"
"default_conferencing_bulk_description": "Update the locations for the selected event types"
}
+16 -6
View File
@@ -1156,7 +1156,8 @@
"event_advanced_tab_title": "Avancé",
"event_setup_multiple_duration_error": "Configuration de l'événement : plusieurs durées requièrent au moins une option.",
"event_setup_multiple_duration_default_error": "Configuration de l'événement : veuillez sélectionner une durée par défaut valide.",
"event_setup_booking_limits_error": "Les limites de réservation doivent être en ordre croissant. [jour,semaine,mois,année]",
"event_setup_booking_limits_error": "Les limites de réservation doivent être dans l'ordre croissant. [jour, semaine, mois, année]",
"event_setup_duration_limits_error": "Les limites de durée doivent être dans l'ordre croissant. [jour, semaine, mois, année]",
"select_which_cal": "Sélectionnez le calendrier auquel ajouter les réservations.",
"custom_event_name": "Nom d'événement personnalisé",
"custom_event_name_description": "Créez des noms d'événement personnalisés à afficher sur l'événement du calendrier",
@@ -1291,7 +1292,7 @@
"exchange_authentication_standard": "Authentification de base",
"exchange_authentication_ntlm": "Authentification NTLM",
"exchange_compression": "Compression GZip",
"routing_forms_description": "Vous pouvez voir tous les formulaires et routes que vous avez créés ici.",
"routing_forms_description": "Créez des formulaires pour diriger les participants vers les bonnes destinations.",
"routing_forms_send_email_owner": "Envoyer un e-mail au propriétaire",
"routing_forms_send_email_owner_description": "Envoie un e-mail au propriétaire lorsque le formulaire est soumis",
"add_new_form": "Ajouter un nouveau formulaire",
@@ -1340,6 +1341,8 @@
"report_app": "Signaler l'application",
"limit_booking_frequency": "Limiter la fréquence de réservation",
"limit_booking_frequency_description": "Limitez le nombre de réservations possibles pour cet événement.",
"limit_total_booking_duration": "Limiter la durée totale de réservation",
"limit_total_booking_duration_description": "Limitez la durée totale pendant laquelle cet événement peut être réservé.",
"add_limit": "Ajouter une limite",
"team_name_required": "Nom d'équipe requis",
"show_attendees": "Partagez les informations des participants entre les invités",
@@ -1443,6 +1446,7 @@
"calcom_is_better_with_team": "Cal.com est meilleur en équipe",
"add_your_team_members": "Ajoutez les membres de votre équipe à vos types d'événements. Utilisez la planification collective pour inclure tout le monde ou trouvez la personne la plus appropriée avec la planification Round-Robin.",
"booking_limit_reached": "La limite de réservation pour ce type d'événement a été atteinte",
"duration_limit_reached": "La limite de durée pour ce type d'événement a été atteinte",
"admin_has_disabled": "Un administrateur a désactivé {{appName}}",
"disabled_app_affects_event_type": "Un administrateur a désactivé {{appName}} qui affecte votre type d'événement {{eventType}}",
"disable_payment_app": "L'administrateur a désactivé {{appName}} qui affecte votre type d'événement {{title}}. Les participants sont toujours en mesure de réserver ce type d'événement, mais ne seront pas invités à payer. Vous pouvez masquer le type d'événement pour l'empêcher jusqu'à ce que votre administrateur réactive votre moyen de paiement.",
@@ -1619,9 +1623,10 @@
"keep_me_connected_with_form": "Rester connecté avec le formulaire",
"fields_in_form_duplicated": "Toute modification du routeur et des champs du formulaire dupliqué se répercutera dans la duplication.",
"form_deleted": "Formulaire supprimé",
"delete_form": "Supprimer le formulaire",
"delete_form": "Voulez-vous vraiment supprimer ce formulaire ?",
"delete_form_action": "Oui, supprimer le formulaire",
"delete_form_confirmation": "Voulez-vous vraiment supprimer ce formulaire ? Toute personne avec qui vous avez partagé le lien ne pourra plus passer de réservation en l'utilisant. De plus, toutes les réponses associées seraient supprimées.",
"delete_form_confirmation": "Toute personne avec qui vous avez partagé le lien ne pourra plus y accéder.",
"delete_form_confirmation_2": "Toutes les réponses associées seront supprimées.",
"typeform_redirect_url_copied": "Lien de redirection Typeform copié ! Vous pouvez aller définir le lien dans le formulaire Typeform.",
"modifications_in_fields_warning": "Les modifications dans les champs et les routes des formulaires suivants seront répercutées dans ce formulaire.",
"connected_forms": "Formulaires connectés",
@@ -1634,10 +1639,15 @@
"add_a_new_route": "Ajouter une nouvelle route",
"no_responses_yet": "Aucune réponse pour l'instant",
"this_will_be_the_placeholder": "Ce sera le texte de substitution",
"this_app_requires_connected_account": "{{appName}} requiert un compte {{dependencyName}} connecté",
"connect_app": "Connecter {{dependencyName}}",
"app_is_connected": "{{dependencyName}} est connectée",
"requires_app": "Requiert {{dependencyName}}",
"verification_code": "Code de vérification",
"can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?",
"verify": "Vérifier",
"invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement",
"select_all": "Tout sélectionner",
"default_conferencing_bulk_title": "Mise à jour multiple des types d'événements existants",
"default_conferencing_bulk_description": "Mettez à jour les emplacements pour les types d'événements sélectionnés.",
"invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement"
"default_conferencing_bulk_description": "Mettez à jour les emplacements pour les types d'événements sélectionnés."
}
+15 -10
View File
@@ -478,8 +478,8 @@
"booking_confirmation": "אשר את ה{{eventTypeTitle}} עם {{profileName}}",
"booking_reschedule_confirmation": "שנה את המועד של ה{{eventTypeTitle}} עם {{profileName}}",
"in_person_meeting": "פגישה אישית",
"attendee_in_person": "פנים-אל-פנים (הכתובת של המשתתף/ת)",
"in_person": "פנים-אל-פנים (הכתובת של המארגן/ת)",
"attendee_in_person": "פנים-אל-פנים (הכתובת של המשתתף)",
"in_person": "פנים-אל-פנים (הכתובת של המארגן)",
"link_meeting": "פגישה עם קישור",
"phone_call": "מספר הטלפון של המשתתף/ת",
"your_number": "מספר הטלפון שלך",
@@ -819,7 +819,7 @@
"installed_app_web3_description": "הגדר באיזה אפליקציות web3 להשתמש בעמודי תזמון הפגישות שלך",
"analytics": "ניתוח נתונים",
"empty_installed_apps_headline": "אין אפליקציות מותקנות",
"empty_installed_apps_description": "אפליקציות מאפשרות לך לשפר משמעותית את תהליכי העבודה שלך ואת תהליך קביעת המועדים.",
"empty_installed_apps_description": "יישומים מאפשרים לך לשפר את תהליכי העבודה שלך ואת פשטות ניהול התזמונים שלך משמעותית.",
"empty_installed_apps_button": "נא לעיין ב-App Store",
"manage_your_connected_apps": "ניהול האפליקציות המותקנות שלך או שינוי הגדרות",
"browse_apps": "עיון באפליקציות",
@@ -1090,7 +1090,7 @@
"hour_one": "{{count}} שעה",
"hour_other": "{{count}} שעות",
"invalid_input": "קלט לא חוקי",
"broken_video_action": "לא יכולנו להוסיף לאירוע המתוכנן שלך את הקישור לפגישה ב-<1>{{location}}</1>. צור/י קשר עם המוזמנים או עדכן/י את הפרטים באירוע בלוח השנה. ניתן <3> לשנות את המיקום בסוג האירוע </3> או לנסות <5>להסיר את האפליקציה ולהוסיף אותה שוב.</5>",
"broken_video_action": "לא יכולנו להוסיף את הקישור <1>{{location}}</1> פגישה שלך לארוע שתוזמן. צור קשר עם המוזמנים שלך או עדכן את הארוע בלוח השנה שלך כדי להוסיף את הפרטים. אתה יכול <3> לשנות את המיקום בסוג הארוע </3> או לנסות <5>להסיר ואז להוסיף את היישום מחדש.</5>",
"broken_calendar_action": "לא הצלחנו לעדכן את <1>{{calendar}}</1>. <2> נא לבדוק את הגדרות לוח השנה שלך או להסיר את לוח השנה ולהוסיף אותו שוב </2>",
"attendee_name": "שם המשתתף",
"scheduler_full_name": "השם המלא של האדם שמבצע את התזמון",
@@ -1463,7 +1463,7 @@
"disable_app": "השבתת האפליקציה",
"disable_app_description": "השבתת האפליקציה הזו עלולה לגרום לבעיות באופן שבו המשתמשים שלך יקיימו אינטראקציה עם Cal",
"edit_keys": "עריכת מפתחות",
"admin_apps_description": "אפשר/י לאפליקציות לפעול עם המופע של Cal",
"admin_apps_description": "אפשר/י לאפליקציות לפעול עם מופע Cal שלך",
"no_available_apps": "אין אפליקציות זמינות",
"no_available_apps_description": "ודא/י שיש אפליקציות בפריסה שלך בנתיב 'packages/app-store'",
"no_apps": "אף אפליקציה לא מופעלת במופע הזה של Cal",
@@ -1480,7 +1480,7 @@
"your_bookings_filter_label": "התזמונים שלך",
"meeting_url_variable": "כתובת ה-URL של הפגישה",
"meeting_url_info": "כתובת ה-URL של שיחת הוועידה באירוע",
"date_overrides": "תאריכים חופפים",
"date_overrides": "חסימת תאריכים",
"date_overrides_subtitle": "הוסף/י תאריכים שבהם הזמינות שלך שונה משעות העבודה הרגילות שלך מדי יום.",
"date_overrides_info": "תאריכים שסומנו למעקף מועברים לארכיון באופן אוטומטי לאחר שהתאריך חולף",
"date_overrides_dialog_which_hours": "באילו שעות את/ה פנוי/ה?",
@@ -1619,9 +1619,10 @@
"keep_me_connected_with_form": "שמור אותי מחובר לטופס",
"fields_in_form_duplicated": "כל שינוי בניתוב ובשדות הטופס שמשוכפל, ישפיעו על הטופס המשוכפל.",
"form_deleted": "הטופס נמחק",
"delete_form": "מחק טופס",
"delete_form": "האם אתה בטוח שאתה רוצה למחוק את הטופס?",
"delete_form_action": "כן, מחק את הטופס",
"delete_form_confirmation": "אתה בטוח שאתה רוצה למחוק את הטופס? כל מי ששיתפת עמו את הקישור לא יוכל יותר לתזמן באמצעותו. בנוסף, כל התגובות הקשורות אליו ימחקו.",
"delete_form_confirmation": "כל מי ששיתפת אתו את הקישור לא יוכל לגשת אליו יותר.",
"delete_form_confirmation_2": "כל התגובות המשויכות ימחקו.",
"typeform_redirect_url_copied": "קישור הפניה TypeForm הועתק! אתה יכול ללכת ולהגדיר את הקישור בטופס ה- Typeform שלך.",
"modifications_in_fields_warning": "שינויים בשדות וניתובים של הטפסים הבאים ישפיעו על טופס זה.",
"connected_forms": "טפסים מחוברים",
@@ -1634,10 +1635,14 @@
"add_a_new_route": "הוסף נתיב חדש",
"no_responses_yet": "אין עדיין תגובות",
"this_will_be_the_placeholder": "זה יהיה הממלא מקום",
"this_app_requires_connected_account": "{{appName}} דורש חשבון {{dependencyName}} מחובר",
"connect_app": "חבר את {{dependencyName}}",
"app_is_connected": "{{dependencyName}} מחובר",
"requires_app": "{{dependencyName}} נדרש",
"verification_code": "קוד אימות",
"verify": "אמת",
"invalid_event_name_variables": "יש משתנה שגוי בשם הארוע שלך",
"select_all": "בחר הכל",
"default_conferencing_bulk_title": "בצע עדכון אצווה של סוגי הארועים הקיימים",
"default_conferencing_bulk_description": "עדכן את המיקומים עבור סוגי הארועים שנבחרו",
"invalid_event_name_variables": "יש משתנה שגוי בשם הארוע שלך"
"default_conferencing_bulk_description": "עדכן את המיקומים עבור סוגי הארועים שנבחרו"
}
@@ -388,7 +388,7 @@
"go_to_billing_portal": "請求ポータルに移動",
"need_anything_else": "他に必要なものはありませんか?",
"further_billing_help": "請求に関するサポートが必要な場合は、サポートチームがお手伝いします。",
"contact": "お問い合わせ",
"contact": "問い合わせる",
"our_support_team": "弊社のサポートチーム",
"contact_our_support_team": "サポートチームに問い合わせる",
"uh_oh": "おっと!",
@@ -465,7 +465,7 @@
"email_address": "メールアドレス",
"enter_valid_email": "有効なメールアドレスを入力してください",
"location": "所在地",
"address": "住所",
"address": "場所",
"enter_address": "住所を入力",
"in_person_attendee_address": "対面(出席者のアドレス)",
"yes": "はい",
@@ -1461,7 +1461,7 @@
"disable_app": "アプリを無効にする",
"disable_app_description": "このアプリを無効にすると、ユーザーが Cal を操作する際に問題が発生する可能性があります",
"edit_keys": "キーを編集",
"admin_apps_description": "Cal のインスタンスにアプリを有効にする",
"admin_apps_description": "アプリを Cal インスタンスに対して有効にする",
"no_available_apps": "利用可能なアプリはありません",
"no_available_apps_description": "\"packages/app-store\" 直下のデプロイメントにアプリがあることをご確認ください",
"no_apps": "Cal のこのインスタンスで有効化されているアプリはありません",
@@ -1491,7 +1491,7 @@
"date_overrides_update_btn": "上書きを更新",
"event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "デフォルトとして設定",
"hide_eventtype_details": "イベントの種類の詳細情報を非表示にする",
"hide_eventtype_details": "EventType の詳細情報を非表示にする",
"show_navigation": "ナビゲーションを表示",
"hide_navigation": "ナビゲーションを非表示にする",
"verification_code_sent": "確認コードが送信されました",
@@ -1504,7 +1504,7 @@
"number_verified": "番号が確認されました",
"create_your_first_team_webhook_description": "このチームイベントの種類の初めての webhook を作成します",
"create_webhook_team_event_type": "このチームイベントの種類の webhook を作成します",
"disable_success_page": "予約の成功ページを無効化する (リダイレクト URL がある場合にのみ有効)",
"disable_success_page": "予約完了ページを無効化する (リダイレクト URL がある場合にのみ有効)",
"invalid_admin_password": "あなたは管理者に設定されていますが、パスワードの長さが不足しています (15 文字以上必要です)",
"change_password_admin": "パスワードを変更して管理者アクセス権を取得する",
"username_already_taken": "ユーザー名が既に使用されています",
+51 -10
View File
@@ -1529,52 +1529,93 @@
"reporting": "Raportowanie",
"reporting_feature": "Zobacz wszystkie elementy przychodzące z danych i pobierz je w formacie CSV",
"teams_plan_required": "Wymagany plan dla zespołów",
"routing_forms_are_a_great_way": "Formularze przekierowania są świetnym sposobem na przekierowanie Twoich przychodzących leadów do właściwej osoby. Przejdź na plan dla zespołów, aby uzyskać dostęp do tej funkcji.",
"routing_forms_are_a_great_way": "Formularze przekierowania są świetnym sposobem na przekierowanie Twoich potencjalnych klientów do właściwej osoby. Przejdź na plan dla zespołów, aby uzyskać dostęp do tej funkcji.",
"choose_a_license": "Wybierz licencję",
"choose_license_description": "Wraz z Cal.com zyskujesz przystępną i bezpłatną licencję AGPLv3 z pewnymi ograniczeniami, która może zostać ulepszona do licencji Enterprise w dowolnym momencie. Możesz wykupić licencję wyższego poziomu w każdej chwili.",
"license": "Licencja",
"agplv3_license": "Licencja AGPLv3",
"enterprise_booking_fee": "Od {{enterprise_booking_fee}}/miesięcznie",
"ee_enterprise_license": "Licencja Enterprise „/ee”",
"enterprise_booking_fee": "Od {{enterprise_booking_fee}}/miesiąc",
"enterprise_license_includes": "Wszystko co niezbędne do zastosowań komercyjnych",
"no_need_to_keep_your_code_open_source": "Twój kod nie musi pozostać na licencji typu open-source.",
"repackage_rebrand_resell": "Łatwa modyfikacja wyglądu, marki i odsprzedaży",
"repackage_rebrand_resell": "Łatwa modyfikacja wyglądu, marki i możliwość odsprzedaży",
"a_vast_suite_of_enterprise_features": "Duży wybór funkcji dla przedsiębiorstw",
"free_license_fee": "0,00 USD/miesiąc",
"forever_open_and_free": "Zawsze za darmo i w otwartym dostępie",
"required_to_keep_your_code_open_source": "Twój kod musi pozostać na licencji typu open-source.",
"cannot_repackage_and_resell": "Nie da się łatwo zmodyfikować wyglądu, zmienić marki i odsprzedać.",
"cannot_repackage_and_resell": "Nie da się łatwo zmodyfikować wyglądu, zmienić marki ani odsprzedać.",
"no_enterprise_features": "Brak funkcji dla przedsiębiorstw",
"step_enterprise_license": "Licencja dla przedsiębiorstw",
"step_enterprise_license": "Licencja Enterprise",
"step_enterprise_license_description": "Wszystko do użytku komercyjnego wraz z prywatnym hostingiem, możliwością zmiany wyglądu, marki i odsprzedaży oraz z dostępem do ekskluzywnych komponentów dla przedsiębiorstw.",
"setup": "Konfiguracja",
"setup_description": "Skonfiguruj wystąpienie Cal.com",
"configure": "Konfiguruj",
"sso_configuration": "Pojedyncze logowanie",
"sso_configuration_description": "Skonfiguruj logowanie jednokrotne SAML/OIDC i zezwól członkom zespołu na logowanie za pomocą dostawcy tożsamości",
"sso_oidc_heading": "Logowanie jednokrotne za pomocą OIDC",
"sso_oidc_description": "Skonfiguruj logowanie jednokrotne OIDC za pomocą wybranego dostawcy tożsamości.",
"sso_oidc_configuration_title": "Konfiguracja OIDC",
"sso_oidc_configuration_description": "Skonfiguruj połączenie OIDC z dostawcą tożsamości. Wymagane informacje znajdziesz u dostawcy tożsamości.",
"sso_oidc_callback_copied": "Skopiowano adres URL odwołania",
"sso_saml_heading": "Logowanie jednokrotne za pomocą SAML",
"sso_saml_description": "Skonfiguruj logowanie jednokrotne SAML za pomocą wybranego dostawcy tożsamości.",
"sso_saml_configuration_title": "Konfiguracja SAML",
"sso_saml_configuration_description": "Skonfiguruj połączenie SAML z dostawcą tożsamości. Wymagane informacje znajdziesz u dostawcy tożsamości.",
"sso_saml_acsurl_copied": "Skopiowano adres URL usług ACS",
"sso_saml_entityid_copied": "Skopiowano identyfikator obiektu",
"sso_connection_created_successfully": "Pomyślnie utworzono konfigurację {{connectionType}}",
"sso_connection_deleted_successfully": "Pomyślnie usunięto konfigurację {{connectionType}}",
"delete_sso_configuration": "Usuń konfigurację {{connectionType}}",
"delete_sso_configuration_confirmation": "Tak, usuń konfigurację {{connectionType}}",
"delete_sso_configuration_confirmation_description": "Czy na pewno chcesz usunąć konfigurację {{connectionType}}? Członkowie zespołu, którzy używają logowania {{connectionType}}, nie będą już mogli uzyskać dostępu do Cal.com.",
"organizer_timezone": "Strefa czasowa organizatora",
"email_no_user_cta": "Załóż swoje konto",
"email_user_cta": "Wyświetl zaproszenie",
"email_no_user_invite_heading": "Zaproszono Cię do dołączenia do zespołu w aplikacji {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} zaprasza Cię do dołączenia do jego zespołu w aplikacji {{appName}}. Aplikacja {{appName}} to narzędzie do planowania wydarzeń, które umożliwi Tobie i Twojemu zespołowi zaplanowanie spotkań bez czasochłonnej wymiany wiadomości.",
"email_no_user_invite_steps_intro": "Przeprowadzimy Cię przez kilka krótkich etapów i wkrótce zaczniesz z przyjemnością korzystać z bezstresowego narzędzia do układania grafików.",
"email_no_user_step_one": "Wybierz nazwę użytkownika",
"email_no_user_step_two": "Połącz swoje konto z kalendarzem",
"email_no_user_step_three": "Ustaw swoją dostępność",
"email_no_user_step_four": "Dołącz do zespołu {{teamName}}",
"email_no_user_signoff": "Zespół {{appName}} życzy pomyślnego układania grafików!",
"impersonation_user_tip": "Zamierzasz podszywać się pod użytkownika, co oznacza, że możesz wprowadzać zmiany w jego imieniu. Zachowaj ostrożność.",
"available_variables": "Dostępne zmienne",
"scheduler": "{Scheduler}",
"recommended_next_steps": "Zalecane następne kroki",
"create_a_managed_event": "Utwórz typ zarządzanego wydarzenia",
"meetings_are_better_with_the_right": "Spotkania idą lepiej, gdy uczestniczą w nich właściwi członkowie zespołu. Zaproś ich teraz.",
"collective_or_roundrobin": "Kolektywnie czy według algorytmu karuzelowego",
"collective_or_roundrobin": "Kolektywnie lub według algorytmu karuzelowego",
"book_your_team_members": "Zarezerwuj wszystkich członków zespołu za pomocą wydarzeń kolektywnych lub przeglądaj dostępne osoby, aby znaleźć tę właściwą za pomocą algorytmu karuzelowego.",
"default_app_link_title": "Ustaw domyślny link do aplikacji",
"default_app_link_description": "Ustawienie domyślnego linku do aplikacji pozwala wszystkim nowo utworzonym rodzajom wydarzeń na używanie ustawionego linku do aplikacji.",
"default_app_link_description": "Ustawienie domyślnego linku do aplikacji pozwala wszystkim nowo utworzonym rodzajom wydarzeń na używanie ustawionego przez Ciebie linku do aplikacji.",
"change_default_conferencing_app": "Ustaw jako domyślne",
"under_maintenance": "Przerwa konserwacyjna",
"under_maintenance_description": "Zespół {{appName}} przeprowadza zaplanowane prace konserwacyjne. Jeśli masz jakieś pytania, skontaktuj się z pomocą techniczną.",
"event_type_seats": "Liczba miejsc: {{numberOfSeats}}",
"booking_with_payment_cancelled": "Płatność za to wydarzenie nie jest już możliwa.",
"booking_with_payment_cancelled_already_paid": "Zwrot za tę rezerwację jest już w drodze.",
"booking_with_payment_cancelled_refunded": "Płatność za tę rezerwację została zwrócona.",
"booking_confirmation_failed": "Potwierdzenie rezerwacji nie powiodło się",
"get_started_zapier_templates": "Zacznij korzystać z szablonów Zapier",
"a_routing_form": "Formularz przekierowania",
"form_description_placeholder": "Opis formularza",
"keep_me_connected_with_form": "Zachowaj połączenie z formularzem",
"fields_in_form_duplicated": "Duplikowanie zmian w przekierowaniu i polach formularza zostaną odzwierciedlone w duplikacie.",
"fields_in_form_duplicated": "Duplikowanie zmian w przekierowaniu i polach formularza zostanie odzwierciedlone w duplikacie.",
"form_deleted": "Formularz usunięty",
"delete_form": "Usuń formularz",
"delete_form_action": "Tak, usuń formularz",
"delete_form_confirmation": "Czy na pewno chcesz usunąć ten formularz? Osoby, którym udostępniono link, nie będą już mogły użyć go do rezerwacji. Ponadto wszystkie powiązane odpowiedzi zostaną usunięte.",
"typeform_redirect_url_copied": "Skopiowano adres URL przekierowania typu Typeform! Możesz teraz skonfigurować adres URL w formacie Typeform.",
"typeform_redirect_url_copied": "Skopiowano adres URL przekierowania z rozwiązania Typeform! Możesz teraz skonfigurować adres URL w formularzu Typeform.",
"modifications_in_fields_warning": "Zmiany w polach i przekierowaniach następujących formularzy zostaną odzwierciedlone w tym formularzu.",
"connected_forms": "Połączone formularze",
"form_modifications_warning": "Zmiany w polach i przekierowaniach tutaj będą miały wpływ na następujące formularze.",
"responses_collection_waiting_description": "Poczekaj na zebranie odpowiedzi. Możesz też przesłać formularz osobiście.",
"this_is_what_your_users_would_see": "To zobaczą Twoi użytkownicy.",
"identifies_name_field": "Identyfikuje pole za pomocą tej nazwy.",
"this_will_be_the_placeholder": "To będzie symbol zastępczy"
"add_1_option_per_line": "Dodaj po jednej opcji w każdym wierszu",
"select_a_router": "Wybierz przekierowanie",
"add_a_new_route": "Dodaj nowe przekierowanie",
"no_responses_yet": "Brak odpowiedzi",
"this_will_be_the_placeholder": "To będzie symbol zastępczy."
}
@@ -7,6 +7,7 @@
"upgrade_now": "Atualizar agora",
"accept_invitation": "Aceitar convite",
"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.",
"calcom_explained_new_user": "Termine de configurar sua conta do {{appName}}! Falta pouco para você solucionar todos os seus problemas de agendamento.",
"have_any_questions": "Precisa de ajuda? Estamos aqui para ajudar.",
"reset_password_subject": "{{appName}}: Instruções para redefinir sua senha",
"event_declined_subject": "Recusado: {{title}} em {{date}}",
@@ -23,6 +24,8 @@
"rejection_reason_description": "Tem certeza de que deseja recusar a reserva? Avisaremos a pessoa que tentou reservar. Se quiser, informe um motivo abaixo.",
"rejection_confirmation": "Recusar a reserva",
"manage_this_event": "Gerenciar este evento",
"invite_team_member": "Convidar membro da equipe",
"invite_team_notifcation_badge": "Inv.",
"your_event_has_been_scheduled": "A sua reunião foi agendada",
"your_event_has_been_scheduled_recurring": "Seu evento recorrente foi agendado",
"accept_our_license": "Aceite a nossa licença alterando a variável no arquivo .env <1>NEXT_PUBLIC_LICENSE_CONSENT</1> para '{{agree}}'.",
@@ -410,6 +413,8 @@
"password_updated_successfully": "Senha atualizada com sucesso",
"password_has_been_changed": "Sua senha foi alterada com sucesso.",
"error_changing_password": "Erro ao alterar senha",
"session_timeout_changed": "Sua configuração da sessão foi atualizada com sucesso.",
"session_timeout_change_error": "Erro ao atualizar a configuração da sessão",
"something_went_wrong": "Ocorreu um erro.",
"something_doesnt_look_right": "Algo parece errado?",
"please_try_again": "Por favor, tente novamente.",
@@ -428,6 +433,7 @@
"password_hint_num": "Contém pelo menos um número",
"invalid_password_hint": "A senha precisa ter pelo menos sete caracteres com pelo menos um número e ser uma combinação de letras maiúsculas e minúsculas",
"incorrect_password": "Senha incorreta.",
"incorrect_username_password": "A senha ou o nome de usuário estão incorretos.",
"24_h": "24h",
"use_setting": "Usar configurações",
"am_pm": "manhã/tarde",
@@ -455,10 +461,11 @@
"slots_load_fail": "Não foi possível carregar os horários disponíveis.",
"additional_guests": "+ Participantes Adicionais",
"your_name": "Seu nome",
"your_full_name": "Seu nome completo",
"email_address": "Endereço de e-mail",
"enter_valid_email": "Insira um e-mail válido",
"location": "Local",
"address": "Endereço",
"address": "Local",
"enter_address": "Insira o endereço",
"in_person_attendee_address": "Pessoalmente (endereço do participante)",
"yes": "sim",
@@ -508,6 +515,8 @@
"admin": "Admin",
"administrator_user": "Usuário administrador",
"lets_create_first_administrator_user": "Vamos criar o primeiro usuário administrador.",
"admin_user_created": "Configuração de usuário administrador",
"admin_user_created_description": "Você já criou um usuário administrador. Já é possível fazer login na sua conta.",
"new_member": "Novo Membro",
"invite": "Convidar",
"add_team_members": "Adicionar membros da equipe",
@@ -657,6 +666,7 @@
"edit_availability": "Editar disponibilidade",
"configure_availability": "Configure os horários em que estiver disponível para reservas.",
"copy_times_to": "Copiar tipos para",
"copy_times_to_tooltip": "Copiar horários para…",
"change_weekly_schedule": "Altere a sua agenda semanal",
"logo": "Logotipo",
"error": "Erro",
@@ -668,6 +678,7 @@
"add_attendees": "Adicionar Participantes",
"show_advanced_settings": "Mostrar Configurações Avançadas",
"event_name": "Nome do Evento",
"event_name_in_calendar": "Nome do evento no calendário",
"event_name_tooltip": "O nome que aparecerá nos calendários",
"meeting_with_user": "Reunião com {{attendeeName}}",
"additional_inputs": "Campos adicionais",
@@ -786,6 +797,7 @@
"number_apps_one": "{{count}} Aplicativo",
"number_apps_other": "{{count}} aplicativos",
"trending_apps": "Apps em alta",
"most_popular": "Mais popular",
"explore_apps": "{{category}} aplicativos",
"installed_apps": "Apps instalados",
"free_to_use_apps": "Grátis",
@@ -796,12 +808,14 @@
"no_category_apps_description_analytics": "Adicione um aplicativo de análise às suas páginas de reservas",
"no_category_apps_description_automation": "Adicione um aplicativo de automação para usar",
"no_category_apps_description_other": "Adicione qualquer outro tipo de aplicativo para fazer todos os tipos de coisas",
"no_category_apps_description_web3": "Adicione um aplicativo web3 para suas páginas de reservas",
"installed_app_calendar_description": "Defina o(s) calendário(s) para verificar se há conflitos e evitar reservas duplas.",
"installed_app_conferencing_description": "Adicione seus aplicativos favoritos de videoconferência para suas reuniões",
"installed_app_payment_description": "Configure quais serviços de processamento de pagamento serão usados no carregamento de seus clientes.",
"installed_app_analytics_description": "Configure quais aplicativos de análise serão usados nas suas páginas de reservas",
"installed_app_other_description": "Todos os seus aplicativos instalados de outras categorias.",
"installed_app_automation_description": "Configure quais aplicativos de automação serão usados",
"installed_app_web3_description": "Configure quais aplicativos web3 serão usados nas suas páginas de reservas",
"analytics": "Análise",
"empty_installed_apps_headline": "Nenhum app instalado",
"empty_installed_apps_description": "Os aplicativos permitem que você melhore o fluxo de trabalho e melhore significativamente sua vida de agendamento.",
@@ -929,6 +943,11 @@
"current_location": "Local atual",
"user_phone": "Seu número de telefone",
"new_location": "Novo local",
"session": "Sessão",
"session_description": "Controle sua sessão da conta",
"session_timeout_after": "Tempo esgotado de sessão após",
"session_timeout": "Tempo esgotado de sessão",
"session_timeout_description": "Invalide sua sessão após um certo tempo.",
"no_location": "Nenhum local definido",
"set_location": "Definir local",
"update_location": "Atualizar local",
@@ -1072,6 +1091,7 @@
"broken_video_action": "Não foi possível adicionar o link da reunião de <1>{{location}}</1> ao seu evento agendado. Entre em contato com seus convidados ou atualize o evento no seu calendário para acrescentar detalhes. Você poderá <3>alterar sua localização no tipo de evento</3> ou tentar <5>remover e adicionar o app novamente.</5>",
"broken_calendar_action": "Não foi possível atualizar seu <1>{{calendar}}</1>. <2> Confira as configurações do seu calendário ou remova e adicione seu calendário novamente </2>",
"attendee_name": "Nome do participante",
"scheduler_full_name": "O nome completo da pessoa que está fazendo a reserva",
"broken_integration": "Integração com falha",
"problem_adding_video_link": "Ocorreu um problema ao adicionar um link de vídeo",
"problem_updating_calendar": "Ocorreu um problema ao atualizar seu calendário",
@@ -1214,6 +1234,7 @@
"connect_automation_apps": "Conecte aplicativos de automação",
"connect_analytics_apps": "Conecte aplicativos de análise",
"connect_other_apps": "Conectar outros aplicativos",
"connect_web3_apps": "Conecte aplicativos web3",
"current_step_of_total": "Passo {{currentStep}} de {{maxSteps}}",
"add_variable": "Adicionar variável",
"custom_phone_number": "Número de telefone personalizado",
@@ -1230,6 +1251,7 @@
"to": "Para",
"workflow_turned_on_successfully": "Fluxo de trabalho {{workflowName}} atualizado para {{offOn}} com sucesso",
"download_responses": "Baixar respostas",
"download_responses_description": "Baixe todas as respostas para seu formulário em formato CSV.",
"download": "Baixar",
"create_your_first_form": "Criar seu primeiro formulário",
"create_your_first_form_description": "Com os Formulários de roteamento, você pode fazer perguntas e encaminhar para a pessoa correta ou o tipo de evento.",
@@ -1271,6 +1293,8 @@
"routing_forms_send_email_owner": "Enviar e-mail ao proprietário",
"routing_forms_send_email_owner_description": "Envia um e-mail ao proprietário após o envio do formulário",
"add_new_form": "Adicionar novo formulário",
"create_your_first_route": "Crie sua primeira rota",
"route_to_the_right_person": "Roteie para a pessoa certa com base nas respostas do seu formulário",
"form_description": "Crie o seu formulário para encaminhar um reservante",
"copy_link_to_form": "Copiar link para formulário",
"theme": "Tema",
@@ -1301,6 +1325,7 @@
"password_reset_leading": "Se não receber um e-mail em breve, verifique se o endereço de e-mail digitado está correto, verifique sua pasta de spam ou entre em contato com o suporte se o problema persistir.",
"password_updated": "Senha atualizada!",
"pending_payment": "Pagamento pendente",
"pending_invites": "Convites pendentes",
"confirmation_page_rainbow": "Proteja seu evento com tokens ou NFTs no Ethereum, Polygon e muito mais.",
"not_on_cal": "Não está na {{appName}}",
"no_calendar_installed": "Nenhum calendário instalado",
@@ -1427,6 +1452,9 @@
"disabled_calendar": "Se você tiver outro calendário instalado, novas reservas serão adicionadas a ele. Caso contrário, conecte um novo calendário para não perder novas reservas.",
"enable_apps": "Ativar aplicativos",
"enable_apps_description": "Ative aplicativos que os usuários podem integrar com o Cal.com",
"purchase_license": "Comprar uma licença",
"already_have_key": "Já tenho uma chave:",
"already_have_key_suggestion": "Copie a variável de ambiente CALCOM_LICENSE_KEY existente aqui.",
"app_is_enabled": "{{appName}} foi ativado",
"app_is_disabled": "{{appName}} foi desativado",
"keys_have_been_saved": "As chaves foram salvas",
@@ -1447,6 +1475,7 @@
"individual": "Individual",
"all_bookings_filter_label": "Todas as reservas",
"all_users_filter_label": "Todos os usuários",
"your_bookings_filter_label": "Suas reservas",
"meeting_url_variable": "URL da reunião",
"meeting_url_info": "O URL da conferência de reunião do evento",
"date_overrides": "Substituições de data",
@@ -1462,7 +1491,7 @@
"date_overrides_update_btn": "Atualizar substituição",
"event_type_duplicate_copy_text": "Cópia de {{slug}}",
"set_as_default": "Definir como padrão",
"hide_eventtype_details": "Ocultar detalhes do tipo de evento",
"hide_eventtype_details": "Ocultar detalhes do EventType",
"show_navigation": "Mostrar navegação",
"hide_navigation": "Ocultar navegação",
"verification_code_sent": "Código de verificação enviado",
@@ -1485,6 +1514,7 @@
"round_robin_hosts": "Hosts de Round-Robin",
"minimum_round_robin_hosts_count": "Número de hosts necessários para participar",
"hosts": "Hosts",
"upgrade_to_enable_feature": "É preciso criar uma equipe para ativar este recurso. Clique para criar uma.",
"new_attendee": "Novo participante",
"awaiting_approval": "Aguardando aprovação",
"requires_google_calendar": "Este aplicativo requer uma conexão com o Google Calendar",
@@ -1493,9 +1523,97 @@
"continue_to_install_google_calendar": "Continuar para instalar o Google Calendar",
"install_google_meet": "Instalar o Google Meet",
"install_google_calendar": "Instalar o Google Calendar",
"sender_name": "Nome do remetente",
"already_invited": "Participante já convidado",
"no_recordings_found": "Nenhum registro encontrado",
"reporting": "Relatando",
"reporting_feature": "Veja todos os dados recebidos e baixe como CSV",
"teams_plan_required": "Plano da equipe obrigatório",
"routing_forms_are_a_great_way": "Formulários de roteamento são uma ótima maneira de direcionar os leads recebidos para a pessoa certa. Atualize para o plano Teams e acesse esse recurso.",
"choose_a_license": "Escolha uma licença",
"choose_license_description": "O Cal.com vem com uma licença AGPLv3 acessível e gratuita, mas tem algumas limitações, que podem ser resolvidas atualizando para a licença Enterprise a qualquer momento. Você poderá fazer isso mais tarde, quando quiser.",
"license": "Licença",
"agplv3_license": "Licença AGPLv3",
"ee_enterprise_license": "“/ee” Licença Enterprise",
"enterprise_booking_fee": "A partir de {{enterprise_booking_fee}}/mês",
"enterprise_license_includes": "Tudo para um caso de uso comercial",
"no_need_to_keep_your_code_open_source": "Não precisa manter seu código em código aberto",
"repackage_rebrand_resell": "Reempacote, troque de marca e revenda com facilidade",
"a_vast_suite_of_enterprise_features": "Uma ampla variedade de recursos empresariais",
"free_license_fee": "$0.00/mês",
"forever_open_and_free": "Aberto e gratuito permanentemente",
"required_to_keep_your_code_open_source": "Obrigatório para manter seu código em código aberto",
"cannot_repackage_and_resell": "Não é possível reempacotar, trocar de marca e revender facilmente",
"no_enterprise_features": "Sem recursos corporativos",
"step_enterprise_license": "Licença Enterprise",
"step_enterprise_license_description": "Tudo para um caso de uso comercial com hospedagem privada, reempacotamento, troca de marca e revenda, além de acesso a componentes corporativos exclusivos.",
"setup": "Configuração",
"setup_description": "Instância de configuração do Cal.com",
"configure": "Configurar",
"sso_configuration": "Logon único",
"sso_configuration_description": "Configure o SAML/OIDC SSO e permita que membros da equipe façam login com um provedor de identidade",
"sso_oidc_heading": "SSO com OIDC",
"sso_oidc_description": "Configure o OIDC SSO com o provedor de identidade de sua escolha.",
"sso_oidc_configuration_title": "Configuração do OIDC",
"sso_oidc_configuration_description": "Configure a conexão do OIDC com seu provedor de identidade. Confira as informações necessárias nele.",
"sso_oidc_callback_copied": "URL de chamada de retorno copiada",
"sso_saml_heading": "SSO com SAML",
"sso_saml_description": "Configure o SAML SSO com o provedor de identidade de sua escolha.",
"sso_saml_configuration_title": "Configuração do SAML",
"sso_saml_configuration_description": "Configure a conexão do SAML com seu provedor de identidade. Confira as informações necessárias nele.",
"sso_saml_acsurl_copied": "URL do ACS copiada",
"sso_saml_entityid_copied": "ID da Entidade copiada",
"sso_connection_created_successfully": "Configuração de {{connectionType}} criada com sucesso",
"sso_connection_deleted_successfully": "Configuração de {{connectionType}} excluída com sucesso",
"delete_sso_configuration": "Excluir a configuração de {{connectionType}}",
"delete_sso_configuration_confirmation": "Sim, excluir a configuração de {{connectionType}}",
"delete_sso_configuration_confirmation_description": "Você tem certeza que deseja remover a configuração de {{connectionType}}? Os membros do seu time que utilizam {{connectionType}} para fazer login não conseguirão acessar o Cal.com.",
"organizer_timezone": "Fuso horário do organizador",
"email_no_user_cta": "Criar sua conta",
"email_user_cta": "Ver convite",
"email_no_user_invite_heading": "Você precisa ter recebido um convite para ingressar em uma equipe de {{appName}}",
"email_no_user_invite_subheading": "Você recebeu um convite de {{invitedBy}} para ingressar em sua equipe em {{appName}}. {{appName}} é um agendador que concilia eventos e permite que sua equipe agende reuniões sem precisar trocar e-mails.",
"email_no_user_invite_steps_intro": "Orientaremos ao longo de alguns passos rápidos para que você comece logo a agendar eventos com sua equipe sem preocupação.",
"email_no_user_step_one": "Escolha seu nome de usuário",
"email_no_user_step_two": "Conecte com sua conta de calendário",
"email_no_user_step_three": "Defina sua disponibilidade",
"email_no_user_step_four": "Ingresse em {{teamName}}",
"email_no_user_signoff": "Divirta-se com os agendamentos, equipe do {{appName}}",
"impersonation_user_tip": "Você está preses a representar um usuário, ou seja, tem possibilidade de realizar mudanças em nome dele. Tenha cuidado.",
"available_variables": "Variáveis disponíveis",
"scheduler": "{Scheduler}",
"recommended_next_steps": "Próximos passos recomendados",
"create_a_managed_event": "Crie um tipo de evento gerenciado",
"meetings_are_better_with_the_right": "Reuniões são melhores com a presença dos membros certos da equipe. Convide-os agora.",
"collective_or_roundrobin": "Coletivo ou round-robin",
"book_your_team_members": "Reúna os membros da sua equipe com eventos coletivos, ou selecione a pessoa certa com o round-robin.",
"default_app_link_title": "Defina um link padrão para o app",
"default_app_link_description": "Definir um link padrão para o app permite que todos os tipos de evento recém-criados usem o link definido para o app.",
"change_default_conferencing_app": "Definir como padrão",
"booking_confirmation_failed": "Falhar ao confirmar reserva"
"under_maintenance": "Serviço interrompido para manutenção",
"under_maintenance_description": "A equipe do {{appName}} está realizando uma manutenção programada. Se tiver alguma dúvida, fale com o suporte.",
"event_type_seats": "{{numberOfSeats}} assentos",
"booking_with_payment_cancelled": "Não é mais possível pagar por este evento",
"booking_with_payment_cancelled_already_paid": "Um reembolso pelo pagamento desta reserva está a caminho.",
"booking_with_payment_cancelled_refunded": "O pagamento desta reserva foi reembolsado.",
"booking_confirmation_failed": "Falhar ao confirmar reserva",
"get_started_zapier_templates": "Comece agora com modelos do Zapier",
"a_routing_form": "Formulário de roteamento",
"form_description_placeholder": "Descrição do formulário",
"keep_me_connected_with_form": "Mantenha minha conexão com o formulário",
"fields_in_form_duplicated": "Qualquer alteração no Roteador e nos campos do formulário a ser duplicado afetará a cópia duplicada.",
"form_deleted": "Formulário excluído",
"delete_form_action": "Sim, excluir formulário",
"typeform_redirect_url_copied": "URL de redirecionamento do Typeform copiada! Você pode acessar e definir a URL no formulário do Typeform.",
"modifications_in_fields_warning": "Modificações nos campos e rotas dos formulários a seguir afetarão este formulário.",
"connected_forms": "Formulários conectados",
"form_modifications_warning": "Os formulários a seguir serão afetados ao modificar os campos e rotas aqui.",
"responses_collection_waiting_description": "Aguarde um pouco até as respostas serem coletadas. Você também pode acessar e enviar o formulário por conta própria.",
"this_is_what_your_users_would_see": "Isto é o que seus usuários verão",
"identifies_name_field": "Identifica campo por este nome.",
"add_1_option_per_line": "Adiciona uma opção por linha",
"select_a_router": "Selecione um roteador",
"add_a_new_route": "Adicione uma nova Rota",
"no_responses_yet": "Sem respostas por enquanto",
"this_will_be_the_placeholder": "Este será o espaço reservado"
}
@@ -465,7 +465,7 @@
"email_address": "Endereço de email",
"enter_valid_email": "Insira um e-mail válido",
"location": "Localização",
"address": "Morada",
"address": "Local",
"enter_address": "Especifique o endereço",
"in_person_attendee_address": "Pessoalmente (endereço do participante)",
"yes": "Sim",
@@ -1113,7 +1113,7 @@
"navigate": "Navegar",
"open": "Abrir",
"close": "Fechar",
"upgrade": "Atualizar",
"upgrade": "Upgrade",
"upgrade_to_access_recordings_title": "Atualizar para aceder a gravações",
"upgrade_to_access_recordings_description": "As gravações só estão disponíveis como parte do nosso plano Equipas. Atualize para começar a gravar as suas chamadas",
"recordings_are_part_of_the_teams_plan": "As gravações fazem parte do plano Equipas",
@@ -1479,7 +1479,7 @@
"meeting_url_variable": "URL ședință",
"meeting_url_info": "Adresa URL a evenimentului ședință tip conferință",
"date_overrides": "Suprascrieri dată",
"date_overrides_subtitle": "Adaugă date atunci când disponibilitatea ta se schimbă în programul zilnic.",
"date_overrides_subtitle": "Adaugă date la care disponibilitatea ta se schimbă în programul zilnic.",
"date_overrides_info": "Datele suprascrise sunt arhivate automat după ce au trecut",
"date_overrides_dialog_which_hours": "Între ce ore ești disponibil/ă?",
"date_overrides_dialog_which_hours_unavailable": "Între ce ore ești ocupat/ă?",
@@ -1534,7 +1534,7 @@
"choose_license_description": "Cal.com include o licență AGPLv3 accesibilă și gratuită, cu unele limitări, însă care poate fi actualizată la o licență Enterprise în orice moment. Puteți face upgrade oricând mai târziu.",
"license": "Licență",
"agplv3_license": "Licență AGPLv3",
"ee_enterprise_license": "Licență Enterprise „/ee”",
"ee_enterprise_license": "Licență „/ee” pentru întreprinderi",
"enterprise_booking_fee": "Începând de la {{enterprise_booking_fee}}/lună",
"enterprise_license_includes": "Totul pentru uz comercial",
"no_need_to_keep_your_code_open_source": "Nu este nevoie să vă păstrați codul open-source",
@@ -1572,8 +1572,8 @@
"email_no_user_cta": "Creează-ți contul",
"email_user_cta": "Vizualizare invitație",
"email_no_user_invite_heading": "Ați fost invitat să faceți parte dintr-o echipă de pe {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} v-a invitat să faceți parte din echipa sa de pe {{appName}}. {{appName}} este un instrument de planificare a evenimentelor care vă permite dvs. și echipei dvs. să programați ședințe fără a face ping-pong prin e-mail.",
"email_no_user_invite_steps_intro": "Vom parcurge împreună câțiva pași simpli și vă veți bucura împreună cu echipa dvs. de planificări fără probleme, în cel mai scurt timp.",
"email_no_user_invite_subheading": "{{invitedBy}} v-a invitat să faceți parte din echipa sa de pe {{appName}}. {{appName}} este un instrument de planificare a evenimentelor, care vă permite dvs. și echipei dvs. să programați ședințe fără a face ping-pong prin e-mail.",
"email_no_user_invite_steps_intro": "Vom parcurge împreună câțiva pași simpli și vă veți bucura alături de echipa dvs. de programări fără probleme, în cel mai scurt timp.",
"email_no_user_step_one": "Alegeți-vă numele de utilizator",
"email_no_user_step_two": "Conectați-vă contul de calendar",
"email_no_user_step_three": "Setați-vă disponibilitatea",
@@ -1597,14 +1597,14 @@
"booking_with_payment_cancelled_already_paid": "Există o rambursare în curs pentru plata acestei rezervări.",
"booking_with_payment_cancelled_refunded": "Plata pentru această rezervare a fost rambursată.",
"booking_confirmation_failed": "Confirmare rezervare nereușită",
"get_started_zapier_templates": "Fă primii pași cu șabloanele Zapier",
"get_started_zapier_templates": "Faceți primii pași cu șabloanele Zapier",
"a_routing_form": "Un formular de redirecționare",
"form_description_placeholder": "Descriere formular",
"keep_me_connected_with_form": "Vreau să primesc actualizări privind formularul",
"fields_in_form_duplicated": "Orice modificări aduse parcursului și câmpurilor din formularul duplicat se vor reflecta în duplicat.",
"form_deleted": "Formular șters",
"delete_form": "Ștergere formular",
"delete_form_action": "Da, ștergeți formularul",
"delete_form_action": "Da, șterg formularul",
"delete_form_confirmation": "Sigur doriți să ștergeți acest formular? Nicio persoană căreia i-ați transmis linkul nu va mai putea efectua rezervări pe baza lui. În plus, toate răspunsurile asociate vor fi șterse.",
"typeform_redirect_url_copied": "URL-ul de redirecționare Typeform a fost copiat! Puteți accesa și seta URL-ul în formularul Typeform.",
"modifications_in_fields_warning": "Modificările aduse câmpurilor și parcursurilor pentru următoarele formulare vor fi reproduse în acest formular.",
@@ -1471,7 +1471,7 @@
"enter_option": "Ange alternativ {{index}}",
"add_an_option": "Lägg till ett alternativ",
"radio": "Menyknapp",
"google_meet_warning": "För att använda Google Meet måste du ställa in din målkalender till en Google-kalender",
"google_meet_warning": "För att använda Google Meet måste du ställa in din målkalender till en Google Kalender",
"individual": "Individ",
"all_bookings_filter_label": "Alla bokningar",
"all_users_filter_label": "Alla användare",
@@ -1491,14 +1491,14 @@
"date_overrides_update_btn": "Uppdatera åsidosättning",
"event_type_duplicate_copy_text": "{{slug}}-kopia",
"set_as_default": "Ange som standard",
"hide_eventtype_details": "Dölj information om händelsetyp",
"hide_eventtype_details": "Dölj EventType-information",
"show_navigation": "Visa navigering",
"hide_navigation": "Dölj navigering",
"verification_code_sent": "Verifieringskod har skickats",
"verified_successfully": "Har verifierats",
"wrong_code": "Fel verifieringskod",
"not_verified": "Har inte verifierats än",
"no_availability_in_month": "Inte tillgänglig i {{month}}",
"no_availability_in_month": "Ingen tillgänglighet i {{month}}",
"view_next_month": "Visa nästa månad",
"send_code": "Skicka kod",
"number_verified": "Numret har verifierats",
@@ -388,7 +388,7 @@
"go_to_billing_portal": "Fatura portalına git",
"need_anything_else": "Başka bir şeye ihtiyacınız var mı?",
"further_billing_help": "Faturalandırma konusunda daha fazla yardıma ihtiyacınız olursa destek ekibimiz size yardımcı olmaya hazır.",
"contact": "İletişim",
"contact": "İletişime geçin",
"our_support_team": "destek ekibimiz",
"contact_our_support_team": "Destek ekibimizle iletişime geçin",
"uh_oh": "Hay aksi!",
@@ -1472,7 +1472,7 @@
"add_an_option": "Bir seçenek ekle",
"radio": "Radio",
"google_meet_warning": "Google Meet'i kullanmak için hedef takviminizi Google Takvimi olarak ayarlamanız gerekir",
"individual": "Bireysel",
"individual": "Birey",
"all_bookings_filter_label": "Tüm Rezervasyonlar",
"all_users_filter_label": "Tüm Kullanıcılar",
"your_bookings_filter_label": "Rezervasyonlarınız",
@@ -1491,7 +1491,7 @@
"date_overrides_update_btn": "更新替代",
"event_type_duplicate_copy_text": "{{slug}}-复制",
"set_as_default": "设置为默认",
"hide_eventtype_details": "隐藏活动类型详细信息",
"hide_eventtype_details": "隐藏 EventType 详细信息",
"show_navigation": "显示导航",
"hide_navigation": "隐藏导航",
"verification_code_sent": "验证码已发送",
@@ -1504,7 +1504,7 @@
"number_verified": "号码已验证",
"create_your_first_team_webhook_description": "为此团队活动类型创建第一个 Webhook",
"create_webhook_team_event_type": "为此团队活动类型创建 Webhook",
"disable_success_page": "禁用成功页面 (仅当有重定向链接时才有效)",
"disable_success_page": "禁用“成功”页面 (仅当有重定向链接时才有效)",
"invalid_admin_password": "您被设置为管理员,但您的密码长度不足 15 个字符",
"change_password_admin": "更改密码以获取管理员访问权限",
"username_already_taken": "用户名已被占用",
@@ -1617,5 +1617,6 @@
"select_a_router": "选择导向器",
"add_a_new_route": "添加新途径",
"no_responses_yet": "尚无回复",
"this_will_be_the_placeholder": "这将是占位符"
"this_will_be_the_placeholder": "这将是占位符",
"can_you_try_again": "可否用不同的时间再次尝试?"
}
@@ -465,7 +465,7 @@
"email_address": "電子郵件位址",
"enter_valid_email": "請輸入有效的電子郵件",
"location": "地點",
"address": "地點",
"address": "地址",
"enter_address": "輸入地址",
"in_person_attendee_address": "親自赴會 (與會者地址)",
"yes": "是",
@@ -1504,7 +1504,7 @@
"number_verified": "號碼已驗證",
"create_your_first_team_webhook_description": "為此團隊活動類型建立第一個 Webhook",
"create_webhook_team_event_type": "為此團隊活動類型建立 Webhook",
"disable_success_page": "停用成功頁面 (只適用於您有重新導向網址時)",
"disable_success_page": "停用「成功」頁面 (只適用於您有重新導向網址時)",
"invalid_admin_password": "您被設為管理者,但您的密碼長度至少須達 15 個字元",
"change_password_admin": "變更密碼以獲得管理員存取權",
"username_already_taken": "已有人使用此使用者名稱",
+6 -1
View File
@@ -1,6 +1,7 @@
import type { GetServerSidePropsContext } from "next";
import superjson from "superjson";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { createProxySSGHelpers } from "@calcom/trpc/react/ssg";
import { createContext } from "@calcom/trpc/server/createContext";
import { appRouter } from "@calcom/trpc/server/routers/_app";
@@ -12,7 +13,11 @@ import { appRouter } from "@calcom/trpc/server/routers/_app";
* Make sure to `return { props: { trpcState: ssr.dehydrate() } }` at the end.
*/
export async function ssrInit(context: GetServerSidePropsContext) {
const ctx = await createContext(context);
const { req, res } = context;
const sessionGetter = () => getServerSession({ req, res });
const ctx = await createContext(context, sessionGetter);
const ssr = createProxySSGHelpers({
router: appRouter,
+12 -12
View File
@@ -1,14 +1,14 @@
import dayjs from "@calcom/dayjs";
import { validateBookingLimitOrder } from "@calcom/lib/isBookingLimits";
import { checkBookingLimits, checkLimit } from "@calcom/lib/server";
import type { BookingLimit } from "@calcom/types/Calendar";
import { validateIntervalLimitOrder } from "@calcom/lib";
import { checkBookingLimits, checkBookingLimit } from "@calcom/lib/server";
import type { IntervalLimit } from "@calcom/types/Calendar";
import { prismaMock } from "../../../../tests/config/singleton";
type Mockdata = {
id: number;
startDate: Date;
bookingLimits: BookingLimit;
bookingLimits: IntervalLimit;
};
const MOCK_DATA: Mockdata = {
@@ -63,7 +63,7 @@ describe("Check Booking Limits Tests", () => {
it("Should handle mutiple limits correctly", async () => {
prismaMock.booking.count.mockResolvedValue(1);
expect(
checkLimit({
checkBookingLimit({
key: "PER_DAY",
limitingNumber: 2,
eventStartDate: MOCK_DATA.startDate,
@@ -72,7 +72,7 @@ describe("Check Booking Limits Tests", () => {
).resolves.not.toThrow();
prismaMock.booking.count.mockResolvedValue(3);
expect(
checkLimit({
checkBookingLimit({
key: "PER_WEEK",
limitingNumber: 2,
eventStartDate: MOCK_DATA.startDate,
@@ -83,7 +83,7 @@ describe("Check Booking Limits Tests", () => {
it("Should return busyTimes when set", async () => {
prismaMock.booking.count.mockResolvedValue(2);
expect(
checkLimit({
checkBookingLimit({
key: "PER_DAY",
limitingNumber: 2,
eventStartDate: MOCK_DATA.startDate,
@@ -99,21 +99,21 @@ describe("Check Booking Limits Tests", () => {
describe("Booking limit validation", () => {
it("Should validate a correct limit", () => {
expect(validateBookingLimitOrder({ PER_DAY: 3, PER_MONTH: 5 })).toBe(true);
expect(validateIntervalLimitOrder({ PER_DAY: 3, PER_MONTH: 5 })).toBe(true);
});
it("Should invalidate an incorrect limit", () => {
expect(validateBookingLimitOrder({ PER_DAY: 9, PER_MONTH: 5 })).toBe(false);
expect(validateIntervalLimitOrder({ PER_DAY: 9, PER_MONTH: 5 })).toBe(false);
});
it("Should validate a correct limit with 'gaps' ", () => {
expect(validateBookingLimitOrder({ PER_DAY: 9, PER_YEAR: 25 })).toBe(true);
expect(validateIntervalLimitOrder({ PER_DAY: 9, PER_YEAR: 25 })).toBe(true);
});
it("Should validate a correct limit with equal values ", () => {
expect(validateBookingLimitOrder({ PER_DAY: 1, PER_YEAR: 1 })).toBe(true);
expect(validateIntervalLimitOrder({ PER_DAY: 1, PER_YEAR: 1 })).toBe(true);
});
it("Should validate a correct with empty", () => {
expect(validateBookingLimitOrder({})).toBe(true);
expect(validateIntervalLimitOrder({})).toBe(true);
});
});
@@ -0,0 +1,128 @@
import dayjs from "@calcom/dayjs";
import { validateIntervalLimitOrder } from "@calcom/lib";
import { checkDurationLimit, checkDurationLimits } from "@calcom/lib/server";
import { prismaMock } from "../../../../tests/config/singleton";
type MockData = {
id: number;
startDate: Date;
};
const MOCK_DATA: MockData = {
id: 1,
startDate: dayjs("2022-09-30T09:00:00+01:00").toDate(),
};
// Path: apps/web/test/lib/checkDurationLimits.ts
describe("Check Duration Limits Tests", () => {
it("Should return no errors if limit is not reached", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 0 }]);
await expect(
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
).resolves.toBeTruthy();
});
it("Should throw an error if limit is reached", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
await expect(
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
).rejects.toThrowError();
});
it("Should pass with multiple duration limits", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 30 }]);
await expect(
checkDurationLimits(
{
PER_DAY: 60,
PER_WEEK: 120,
},
MOCK_DATA.startDate,
MOCK_DATA.id
)
).resolves.toBeTruthy();
});
it("Should pass with multiple duration limits with one undefined", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 30 }]);
await expect(
checkDurationLimits(
{
PER_DAY: 60,
PER_WEEK: undefined,
},
MOCK_DATA.startDate,
MOCK_DATA.id
)
).resolves.toBeTruthy();
});
it("Should return no errors if limit is not reached with multiple bookings", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
await expect(
checkDurationLimits(
{
PER_DAY: 90,
PER_WEEK: 120,
},
MOCK_DATA.startDate,
MOCK_DATA.id
)
).resolves.toBeTruthy();
});
it("Should throw an error if one of the limit is reached with multiple bookings", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 90 }]);
await expect(
checkDurationLimits(
{
PER_DAY: 60,
PER_WEEK: 120,
},
MOCK_DATA.startDate,
MOCK_DATA.id
)
).rejects.toThrowError();
});
});
// Path: apps/web/test/lib/checkDurationLimits.ts
describe("Check Duration Limit Tests", () => {
it("Should return no busyTimes and no error if limit is not reached", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
await expect(
checkDurationLimit({
key: "PER_DAY",
limitingNumber: 90,
eventStartDate: MOCK_DATA.startDate,
eventId: MOCK_DATA.id,
})
).resolves.toBeUndefined();
});
it("Should return busyTimes when set and limit is reached", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
await expect(
checkDurationLimit({
key: "PER_DAY",
limitingNumber: 60,
eventStartDate: MOCK_DATA.startDate,
eventId: MOCK_DATA.id,
returnBusyTimes: true,
})
).resolves.toEqual({
start: dayjs(MOCK_DATA.startDate).startOf("day").toDate(),
end: dayjs(MOCK_DATA.startDate).endOf("day").toDate(),
});
});
});
describe("Duration limit validation", () => {
it("Should validate limit where ranges have ascending values", () => {
expect(validateIntervalLimitOrder({ PER_DAY: 30, PER_MONTH: 60 })).toBe(true);
});
it("Should invalidate limit where ranges does not have a strict ascending values", () => {
expect(validateIntervalLimitOrder({ PER_DAY: 60, PER_WEEK: 30 })).toBe(false);
});
it("Should validate a correct limit with 'gaps'", () => {
expect(validateIntervalLimitOrder({ PER_DAY: 60, PER_YEAR: 120 })).toBe(true);
});
it("Should validate empty limit", () => {
expect(validateIntervalLimitOrder({})).toBe(true);
});
});
+5
View File
@@ -1,3 +1,7 @@
/**
* !: Stops the `jose` dependency from bundling the browser version and breaking tests
* @jest-environment node
*/
import type {
EventType as PrismaEventType,
User as PrismaUser,
@@ -504,6 +508,7 @@ describe("getSchedule", () => {
);
});
// FIXME: Fix minimumBookingNotice is respected test
test.skip("minimumBookingNotice is respected", async () => {
jest.useFakeTimers().setSystemTime(
(() => {
+7 -1
View File
@@ -35,9 +35,15 @@ const config: Config = {
displayName: "@calcom/lib",
roots: ["<rootDir>/packages/lib"],
testEnvironment: "node",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
transform: {
"^.+\\.tsx?$": "ts-jest",
},
globals: {
"ts-jest": {
tsconfig: "<rootDir>/packages/lib/tsconfig.test.json",
},
},
},
{
displayName: "@calcom/closecom",
@@ -52,7 +58,7 @@ const config: Config = {
},
{
displayName: "@calcom/routing-forms",
roots: ["<rootDir>/packages/app-store/ee/routing-forms"],
roots: ["<rootDir>/packages/app-store/routing-forms"],
testMatch: ["**/test/lib/**/*.(spec|test).(ts|tsx|js)"],
transform: {
"^.+\\.ts?$": "ts-jest",
+4
View File
@@ -39,6 +39,10 @@
"docs-start": "turbo run start --scope=\"@calcom/docs\"",
"build-storybook": "turbo run build --scope=\"@calcom/storybook\"",
"dx": "turbo run dx",
"i-dev": "infisical run -- turbo run dev --scope=\"@calcom/web\"",
"i-dx": "infisical run -- turbo run dx",
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
"embed-tests-quick": "turbo run embed-tests-quick",
"embed-tests": "turbo run embed-tests",
"env-check:app-store": "dotenv-checker --schema .env.appStore.example --env .env.appStore",
+18
View File
@@ -1,4 +1,5 @@
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { getAppFromSlug } from "@calcom/app-store/utils";
import prisma, { safeAppSelect, safeCredentialSelect } from "@calcom/prisma";
import { userMetadata } from "@calcom/prisma/zod-utils";
import type { AppFrontendPayload as App } from "@calcom/types/App";
@@ -82,6 +83,21 @@ export async function getAppRegistryWithCredentials(userId: number) {
// Skip if app isn't installed
/* This is now handled from the DB */
// if (!app.installed) return apps;
let dependencyData: {
name?: string;
installed?: boolean;
}[] = [];
if (app.dependencies) {
dependencyData = app.dependencies.map((dependency) => {
const dependencyInstalled = dbApps.some(
(dbAppIterator) => dbAppIterator.credentials.length && dbAppIterator.slug === dependency
);
// If the app marked as dependency is simply deleted from the codebase, we can have the situation where App is marked installed in DB but we couldn't get the app.
const dependencyName = getAppFromSlug(dependency)?.name;
return { name: dependencyName, installed: dependencyInstalled };
});
}
const { rating, reviews, trending, verified, ...remainingAppProps } = app;
apps.push({
rating: rating || 0,
@@ -93,7 +109,9 @@ export async function getAppRegistryWithCredentials(userId: number) {
credentials: dbapp.credentials,
installed: true,
isDefault: usersDefaultApp === dbapp.slug,
...(app.dependencies && { dependencyData }),
});
}
return apps;
}
+2 -1
View File
@@ -11,5 +11,6 @@
"publisher": "Cal.com, Inc.",
"email": "support@cal.com",
"description": "The joyful productivity app\r\r",
"__createdUsingCli": true
"__createdUsingCli": true,
"dependencies": ["google-calendar"]
}
@@ -13,7 +13,6 @@ export const InstallAppButtonMap = {
exchange2016calendar: dynamic(() => import("./exchange2016calendar/components/InstallAppButton")),
exchangecalendar: dynamic(() => import("./exchangecalendar/components/InstallAppButton")),
googlecalendar: dynamic(() => import("./googlecalendar/components/InstallAppButton")),
googlevideo: dynamic(() => import("./googlevideo/components/InstallAppButton")),
hubspot: dynamic(() => import("./hubspot/components/InstallAppButton")),
huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")),
jitsivideo: dynamic(() => import("./jitsivideo/components/InstallAppButton")),
@@ -3,7 +3,6 @@
Don't modify this file manually.
**/
import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
import { appKeysSchema as routing_forms_zod_ts } from "./ee/routing-forms/zod";
import { appKeysSchema as fathom_zod_ts } from "./fathom/zod";
import { appKeysSchema as ga4_zod_ts } from "./ga4/zod";
import { appKeysSchema as giphy_zod_ts } from "./giphy/zod";
@@ -15,6 +14,7 @@ import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
import { appKeysSchema as plausible_zod_ts } from "./plausible/zod";
import { appKeysSchema as qr_code_zod_ts } from "./qr_code/zod";
import { appKeysSchema as rainbow_zod_ts } from "./rainbow/zod";
import { appKeysSchema as routing_forms_zod_ts } from "./routing-forms/zod";
import { appKeysSchema as salesforce_zod_ts } from "./salesforce/zod";
import { appKeysSchema as stripepayment_zod_ts } from "./stripepayment/zod";
import { appKeysSchema as tandemvideo_zod_ts } from "./tandemvideo/zod";
@@ -27,7 +27,6 @@ import { appKeysSchema as zoomvideo_zod_ts } from "./zoomvideo/zod";
export const appKeysSchemas = {
dailyvideo: dailyvideo_zod_ts,
"routing-forms": routing_forms_zod_ts,
fathom: fathom_zod_ts,
ga4: ga4_zod_ts,
giphy: giphy_zod_ts,
@@ -39,6 +38,7 @@ export const appKeysSchemas = {
plausible: plausible_zod_ts,
qr_code: qr_code_zod_ts,
rainbow: rainbow_zod_ts,
"routing-forms": routing_forms_zod_ts,
salesforce: salesforce_zod_ts,
stripe: stripepayment_zod_ts,
tandemvideo: tandemvideo_zod_ts,
@@ -8,8 +8,8 @@ import around_config_json from "./around/config.json";
import { metadata as caldavcalendar__metadata_ts } from "./caldavcalendar/_metadata";
import campfire_config_json from "./campfire/config.json";
import closecom_config_json from "./closecom/config.json";
import cron_config_json from "./cron/config.json";
import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata";
import routing_forms_config_json from "./ee/routing-forms/config.json";
import { metadata as exchange2013calendar__metadata_ts } from "./exchange2013calendar/_metadata";
import { metadata as exchange2016calendar__metadata_ts } from "./exchange2016calendar/_metadata";
import exchangecalendar_config_json from "./exchangecalendar/config.json";
@@ -33,6 +33,7 @@ import qr_code_config_json from "./qr_code/config.json";
import rainbow_config_json from "./rainbow/config.json";
import raycast_config_json from "./raycast/config.json";
import riverside_config_json from "./riverside/config.json";
import routing_forms_config_json from "./routing-forms/config.json";
import salesforce_config_json from "./salesforce/config.json";
import sendgrid_config_json from "./sendgrid/config.json";
import signal_config_json from "./signal/config.json";
@@ -64,8 +65,8 @@ export const appStoreMetadata = {
caldavcalendar: caldavcalendar__metadata_ts,
campfire: campfire_config_json,
closecom: closecom_config_json,
cron: cron_config_json,
dailyvideo: dailyvideo__metadata_ts,
"routing-forms": routing_forms_config_json,
exchange2013calendar: exchange2013calendar__metadata_ts,
exchange2016calendar: exchange2016calendar__metadata_ts,
exchangecalendar: exchangecalendar_config_json,
@@ -89,6 +90,7 @@ export const appStoreMetadata = {
rainbow: rainbow_config_json,
raycast: raycast_config_json,
riverside: riverside_config_json,
"routing-forms": routing_forms_config_json,
salesforce: salesforce_config_json,
sendgrid: sendgrid_config_json,
signal: signal_config_json,
+2 -2
View File
@@ -3,7 +3,6 @@
Don't modify this file manually.
**/
import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
import { appDataSchema as routing_forms_zod_ts } from "./ee/routing-forms/zod";
import { appDataSchema as fathom_zod_ts } from "./fathom/zod";
import { appDataSchema as ga4_zod_ts } from "./ga4/zod";
import { appDataSchema as giphy_zod_ts } from "./giphy/zod";
@@ -15,6 +14,7 @@ import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
import { appDataSchema as plausible_zod_ts } from "./plausible/zod";
import { appDataSchema as qr_code_zod_ts } from "./qr_code/zod";
import { appDataSchema as rainbow_zod_ts } from "./rainbow/zod";
import { appDataSchema as routing_forms_zod_ts } from "./routing-forms/zod";
import { appDataSchema as salesforce_zod_ts } from "./salesforce/zod";
import { appDataSchema as stripepayment_zod_ts } from "./stripepayment/zod";
import { appDataSchema as tandemvideo_zod_ts } from "./tandemvideo/zod";
@@ -27,7 +27,6 @@ import { appDataSchema as zoomvideo_zod_ts } from "./zoomvideo/zod";
export const appDataSchemas = {
dailyvideo: dailyvideo_zod_ts,
"routing-forms": routing_forms_zod_ts,
fathom: fathom_zod_ts,
ga4: ga4_zod_ts,
giphy: giphy_zod_ts,
@@ -39,6 +38,7 @@ export const appDataSchemas = {
plausible: plausible_zod_ts,
qr_code: qr_code_zod_ts,
rainbow: rainbow_zod_ts,
"routing-forms": routing_forms_zod_ts,
salesforce: salesforce_zod_ts,
stripe: stripepayment_zod_ts,
tandemvideo: tandemvideo_zod_ts,
+2 -1
View File
@@ -9,7 +9,7 @@ export const apiHandlers = {
caldavcalendar: import("./caldavcalendar/api"),
campfire: import("./campfire/api"),
closecom: import("./closecom/api"),
"routing-forms": import("./ee/routing-forms/api"),
cron: import("./cron/api"),
exchange2013calendar: import("./exchange2013calendar/api"),
exchange2016calendar: import("./exchange2016calendar/api"),
exchangecalendar: import("./exchangecalendar/api"),
@@ -33,6 +33,7 @@ export const apiHandlers = {
rainbow: import("./rainbow/api"),
raycast: import("./raycast/api"),
riverside: import("./riverside/api"),
"routing-forms": import("./routing-forms/api"),
salesforce: import("./salesforce/api"),
sendgrid: import("./sendgrid/api"),
signal: import("./signal/api"),
+92 -2
View File
@@ -1,10 +1,16 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useRef } from "react";
import classNames from "@calcom/lib/classNames";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { CAL_URL } from "@calcom/lib/constants";
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App";
import { FiAlertCircle, FiArrowRight, FiCheck } from "@calcom/ui/components/icon";
import { InstallAppButtonMap } from "./apps.browser.generated";
import type { InstallAppButtonProps } from "./types";
@@ -16,9 +22,16 @@ export const InstallAppButtonWithoutPlanCheck = (
) => {
const key = deriveAppDictKeyFromType(props.type, InstallAppButtonMap);
const InstallAppButtonComponent = InstallAppButtonMap[key as keyof typeof InstallAppButtonMap];
if (!InstallAppButtonComponent) return <>{props.render({ useDefaultComponent: true })}</>;
if (!InstallAppButtonComponent)
return <>{props.render({ useDefaultComponent: true, disabled: props.disableInstall })}</>;
return <InstallAppButtonComponent render={props.render} onChanged={props.onChanged} />;
return (
<InstallAppButtonComponent
render={props.render}
onChanged={props.onChanged}
disableInstall={props.disableInstall}
/>
);
};
export const InstallAppButton = (
@@ -26,6 +39,7 @@ export const InstallAppButton = (
isProOnly?: App["isProOnly"];
type: App["type"];
wrapperClassName?: string;
disableInstall?: boolean;
} & InstallAppButtonProps
) => {
const { isLoading, data: user } = trpc.viewer.me.useQuery();
@@ -63,3 +77,79 @@ export const InstallAppButton = (
};
export { AppConfiguration } from "./_components/AppConfiguration";
export const AppDependencyComponent = ({
appName,
dependencyData,
}: {
appName: string;
dependencyData: RouterOutputs["viewer"]["appsRouter"]["queryForDependencies"];
}) => {
const { t } = useLocale();
return (
<div
className={classNames(
"rounded-md py-3 px-4",
dependencyData && dependencyData.some((dependency) => !dependency.installed)
? "bg-blue-100"
: "bg-gray-100"
)}>
{dependencyData &&
dependencyData.map((dependency) => {
return dependency.installed ? (
<div className="items-start space-x-2.5">
<div className="flex items-start">
<div>
<FiCheck className="mt-1 mr-2 font-semibold" />
</div>
<div>
<span className="font-semibold">
{t("app_is_connected", { dependencyName: dependency.name })}
</span>
<div>
<div>
<span>
{t("this_app_requires_connected_account", {
appName,
dependencyName: dependency.name,
})}
</span>
</div>
</div>
</div>
</div>
</div>
) : (
<div className="items-start space-x-2.5">
<div className="flex items-start text-blue-900">
<div>
<FiAlertCircle className="mt-1 mr-2 font-semibold" />
</div>
<div>
<span className="font-semibold">
{t("this_app_requires_connected_account", { appName, dependencyName: dependency.name })}
</span>
<div>
<div>
<>
<Link
href={`${CAL_URL}/apps/${dependency.slug}`}
className="flex items-center text-blue-900 underline">
<span className="mr-1">
{t("connect_app", { dependencyName: dependency.name })}
</span>
<FiArrowRight />
</Link>
</>
</div>
</div>
</div>
</div>
</div>
);
})}
</div>
);
};
+21
View File
@@ -0,0 +1,21 @@
---
items:
- 1.png
- 2.png
- 3.png
- 4.png
- 5.png
---
{DESCRIPTION}
### Cron's most valuable features
<!-- From "FAQs: What are Cron’s most valuable features?"
https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 -->
- **Availability sharing** — mark holds on the grid, share the slots, and schedule with ease.
- **Team overlays** — quickly overlay teammates' calendars.
- **Time zones** — add as many as needed, drag & drop to quickly "travel" to places.
- **Multiple accounts** — unify work and personal calendars in one place.
- **Menu bar calendar** — tightly integrated; see and join upcoming meetings in seconds.
- **Speed, multi-select flows, keyboard shortcuts, and gorgeous light/dark modes.**
+20
View File
@@ -0,0 +1,20 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
redirect: {
newTab: true,
url: "https://cron.com",
},
createCredential: ({ appType, user, slug }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
};
export default handler;
+1
View File
@@ -0,0 +1 @@
export { default as add } from "./add";
+18
View File
@@ -0,0 +1,18 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Cron",
"slug": "cron",
"type": "cron_other",
"imageSrc": "logo.png",
"logo": "logo.png",
"url": "https://cal.com/apps/cron",
"variant": "other",
"categories": ["calendar"],
"publisher": "miguelnietoa",
"email": "support@cal.com",
"description": "Cron is the next-generation calendar for professionals and teams.",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "link-as-an-app",
"dependencies": ["google-calendar"]
}
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/cron",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "Cron is the next-generation calendar for professionals and teams."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

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